Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1523d1b5b2 | ||
|
|
ea14e28dd8 | ||
|
|
d7750086b5 | ||
|
|
eb7216b4b1 | ||
|
|
1d62225019 | ||
|
|
bf5250c4a7 | ||
|
|
cdd0a864d1 | ||
|
|
692c1bef25 | ||
|
|
fe09d0e25f | ||
|
|
3ce18dc1dc | ||
|
|
6fb31abd84 | ||
|
|
7a301b455c | ||
|
|
5781a7b540 | ||
|
|
2efbeee5bf | ||
|
|
be4ff0f0f0 | ||
|
|
3ea52a24ef | ||
|
|
f179189b59 | ||
|
|
bc333f768f | ||
|
|
26c98e2b41 | ||
|
|
4b7732838d | ||
|
|
021b9ac5d5 | ||
|
|
5f94c7295a | ||
|
|
d9a9523c9a | ||
|
|
384d2343d5 | ||
|
|
7a387d86d6 | ||
|
|
330eef51e4 | ||
|
|
2e1a2b60c4 | ||
|
|
9896ca0e8c | ||
|
|
fd44f42f28 | ||
|
|
966ce261cb | ||
|
|
5503e7a54f | ||
|
|
b49ec93c54 | ||
|
|
e6584322fa | ||
|
|
1138cd02b0 | ||
|
|
1ba8d0cbd1 | ||
|
|
98380e2d48 | ||
|
|
e377ed10e1 | ||
|
|
acc35f4717 | ||
|
|
33742a043d | ||
|
|
5cee9cbbb9 | ||
|
|
e0fbbeca3c | ||
|
|
bbfc9d6f14 | ||
|
|
46b6e56c06 | ||
|
|
05f6582739 | ||
|
|
119b1ec840 | ||
|
|
e89e313712 | ||
|
|
c2ff658182 | ||
|
|
02cd98fa9c | ||
|
|
d327bc355c | ||
|
|
8ae631a626 | ||
|
|
1d8dc8fe5b | ||
|
|
688ae6c86e | ||
|
|
21809843cd |
23
.github/workflows/close_inactive_issues.yml
vendored
23
.github/workflows/close_inactive_issues.yml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "00 00 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 14
|
||||
exempt-issue-labels: "feature"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
98
CHANGELOG.md
98
CHANGELOG.md
@@ -1,3 +1,101 @@
|
||||
## [0.21.4](https://github.com/stonith404/pingvin-share/compare/v0.21.3...v0.21.4) (2024-01-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **frontend:** add navigateToLink button for CopyTextField. close [#372](https://github.com/stonith404/pingvin-share/issues/372). ([#376](https://github.com/stonith404/pingvin-share/issues/376)) ([d775008](https://github.com/stonith404/pingvin-share/commit/d7750086b5b796cfc70d8dc0c7d0ab4bd1996ca0))
|
||||
|
||||
## [0.21.3](https://github.com/stonith404/pingvin-share/compare/v0.21.2...v0.21.3) (2024-01-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't show validation error on upload modal if password or max views are empty ([fe09d0e](https://github.com/stonith404/pingvin-share/commit/fe09d0e25f6fbfc4e1c9302054d3387fe8b1f0ea))
|
||||
|
||||
## [0.21.2](https://github.com/stonith404/pingvin-share/compare/v0.21.1...v0.21.2) (2023-12-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* missing logo images on fresh installation ([6fb31ab](https://github.com/stonith404/pingvin-share/commit/6fb31abd84b22cd464b6b45bf7ca6f83853e8720))
|
||||
* missing translations on reset password page ([7a301b4](https://github.com/stonith404/pingvin-share/commit/7a301b455cdea4b1dbc04cc6223e094fee9aca7b))
|
||||
|
||||
## [0.21.1](https://github.com/stonith404/pingvin-share/compare/v0.21.0...v0.21.1) (2023-12-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **oauth:** add oidc username claim ([#357](https://github.com/stonith404/pingvin-share/issues/357)) ([3ea52a2](https://github.com/stonith404/pingvin-share/commit/3ea52a24ef7c3b6845bc13382616ea0c8d784585))
|
||||
|
||||
## [0.21.0](https://github.com/stonith404/pingvin-share/compare/v0.20.3...v0.21.0) (2023-12-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **oauth:** limited discord server sign-in ([#346](https://github.com/stonith404/pingvin-share/issues/346)) ([5f94c72](https://github.com/stonith404/pingvin-share/commit/5f94c7295ab8594ed2ed615628214e869a02da2d))
|
||||
|
||||
## [0.20.3](https://github.com/stonith404/pingvin-share/compare/v0.20.2...v0.20.3) (2023-11-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* max expiration gets ignored if expiration is set to "never" ([330eef5](https://github.com/stonith404/pingvin-share/commit/330eef51e4f3f3fb29833bc9337e705553340aaa))
|
||||
|
||||
## [0.20.2](https://github.com/stonith404/pingvin-share/compare/v0.20.1...v0.20.2) (2023-11-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **oauth:** github and discord login error ([#323](https://github.com/stonith404/pingvin-share/issues/323)) ([fd44f42](https://github.com/stonith404/pingvin-share/commit/fd44f42f28c0fa2091876b138f170202d9fde04e)), closes [#322](https://github.com/stonith404/pingvin-share/issues/322) [#302](https://github.com/stonith404/pingvin-share/issues/302)
|
||||
* reverse shares couldn't be created unauthenticated ([966ce26](https://github.com/stonith404/pingvin-share/commit/966ce261cb4ad99efaadef5c36564fdfaed0d5c4))
|
||||
|
||||
## [0.20.1](https://github.com/stonith404/pingvin-share/compare/v0.20.0...v0.20.1) (2023-11-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* share information text color in light mode ([1138cd0](https://github.com/stonith404/pingvin-share/commit/1138cd02b0b6ac1d71c4dbc2808110c672237190))
|
||||
|
||||
## [0.20.0](https://github.com/stonith404/pingvin-share/compare/v0.19.2...v0.20.0) (2023-11-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ability to add and delete files of existing share ([#306](https://github.com/stonith404/pingvin-share/issues/306)) ([98380e2](https://github.com/stonith404/pingvin-share/commit/98380e2d48cc8ffa831d9b69cf5c0e8a40e28862))
|
||||
|
||||
## [0.19.2](https://github.com/stonith404/pingvin-share/compare/v0.19.1...v0.19.2) (2023-11-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* ability to limit the max expiration of a share ([bbfc9d6](https://github.com/stonith404/pingvin-share/commit/bbfc9d6f147eea404f011c3af9d7dc7655c3d21d))
|
||||
* change totp issuer to display logo in 2FAS app ([e0fbbec](https://github.com/stonith404/pingvin-share/commit/e0fbbeca3c1a858838b20aeead52694772b7d871))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* jwt secret changes on application restart ([33742a0](https://github.com/stonith404/pingvin-share/commit/33742a043d6549783984ae7e8a3c30f0fe3917de))
|
||||
* wrong validation of setting max share expiration to `0` ([acc35f4](https://github.com/stonith404/pingvin-share/commit/acc35f47178e230f50ce54d6f1ad5370caa3382d))
|
||||
|
||||
## [0.19.1](https://github.com/stonith404/pingvin-share/compare/v0.19.0...v0.19.1) (2023-10-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **oauth:** fix wrong redirectUri in oidc after change appUrl ([#296](https://github.com/stonith404/pingvin-share/issues/296)) ([119b1ec](https://github.com/stonith404/pingvin-share/commit/119b1ec840ad7f4e1c7c4bb476bf1eeed91d9a1a))
|
||||
|
||||
## [0.19.0](https://github.com/stonith404/pingvin-share/compare/v0.18.2...v0.19.0) (2023-10-22)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **auth:** add OAuth2 login ([#276](https://github.com/stonith404/pingvin-share/issues/276)) ([02cd98f](https://github.com/stonith404/pingvin-share/commit/02cd98fa9cf9865d91494848aabaf42b19e4957b)), closes [#278](https://github.com/stonith404/pingvin-share/issues/278) [#279](https://github.com/stonith404/pingvin-share/issues/279) [#281](https://github.com/stonith404/pingvin-share/issues/281)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* delete unfinished shares after a day ([d327bc3](https://github.com/stonith404/pingvin-share/commit/d327bc355c8583231e058731934cf51ab25d9ce5))
|
||||
|
||||
## [0.18.2](https://github.com/stonith404/pingvin-share/compare/v0.18.1...v0.18.2) (2023-10-09)
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ RUN npm run build
|
||||
|
||||
# Stage 3: Backend dependencies
|
||||
FROM node:20-alpine AS backend-dependencies
|
||||
RUN apk add --no-cache python3
|
||||
WORKDIR /opt/app
|
||||
COPY backend/package.json backend/package-lock.json ./
|
||||
RUN npm ci
|
||||
@@ -57,4 +58,4 @@ HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/he
|
||||
|
||||
# Application startup
|
||||
# HOSTNAME=0.0.0.0 fixes https://github.com/vercel/next.js/issues/51684. It can be removed as soon as the issue is fixed
|
||||
CMD cp -rn /tmp/img /opt/app/frontend/public && nginx && PORT=3333 HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod
|
||||
CMD cp -rn /tmp/img/* /opt/app/frontend/public/img && nginx && PORT=3333 HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
|
||||
_Read this in another language: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md)_
|
||||
_Read this in another language: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
@@ -79,6 +79,10 @@ ClamAV is used to scan shares for malicious files and remove them if found.
|
||||
|
||||
Please note that ClamAV needs a lot of [ressources](https://docs.clamav.net/manual/Installing/Docker.html#memory-ram-requirements).
|
||||
|
||||
#### OAuth 2 Login
|
||||
|
||||
View the [OAuth 2 guide](/docs/oauth2-guide.md) for more information.
|
||||
|
||||
### Additional resources
|
||||
|
||||
- [Synology NAS installation](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
284
backend/package-lock.json
generated
284
backend/package-lock.json
generated
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.18.2",
|
||||
"version": "0.21.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.18.2",
|
||||
"version": "0.21.4",
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/common": "^10.1.2",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.2",
|
||||
@@ -21,6 +22,7 @@
|
||||
"archiver": "^5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cache-manager": "^5.2.4",
|
||||
"clamscan": "^2.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
@@ -28,6 +30,8 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^6.9.4",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
@@ -52,6 +56,7 @@
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/node-fetch": "^2.6.6",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/qrcode-svg": "^1.1.1",
|
||||
@@ -622,6 +627,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cache-manager": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/cache-manager/-/cache-manager-2.1.0.tgz",
|
||||
"integrity": "sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||
"cache-manager": "<=5",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"rxjs": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.10.tgz",
|
||||
@@ -1291,24 +1308,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/clamscan": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.4.tgz",
|
||||
"integrity": "sha512-NpD+EmE+ZK5WRJOAmeDuSYJIv15BUnc4PxQA+m3QNkutaPBZ7bmLDTvqBu2iDchs7YKQjiEQEwEMvsdwtdtImA==",
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.7.tgz",
|
||||
"integrity": "sha512-YopQF+D1pqbvMbvqTQx2MdxEqFbQAiPJKtjj/KnK9xxgmYkdt3dInkj/k6a+bztT4TexQ+tCUi/5D3LqDZIdhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"axios": "^0.24.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/clamscan/node_modules/axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
@@ -1438,6 +1446,16 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
|
||||
"integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg=="
|
||||
},
|
||||
"node_modules/@types/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
@@ -2250,13 +2268,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
@@ -2525,6 +2542,23 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/cache-manager/-/cache-manager-5.2.4.tgz",
|
||||
"integrity": "sha512-gkuCjug16NdGvKm/sydxGVx17uffrSWcEe2xraBtwRCgdYcFxwJAla4OYpASAZT2yhSoxgDiWL9XH6IAChcZJA==",
|
||||
"dependencies": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lru-cache": "^10.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cache-manager/node_modules/lru-cache": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.0.1.tgz",
|
||||
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
@@ -5025,15 +5059,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/joi": {
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
|
||||
"integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
|
||||
"version": "17.11.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz",
|
||||
"integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@hapi/hoek": "^9.0.0",
|
||||
"@hapi/topo": "^5.0.0",
|
||||
"@sideway/address": "^4.1.3",
|
||||
"@sideway/formula": "^3.0.0",
|
||||
"@sideway/formula": "^3.0.1",
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -5248,6 +5282,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
@@ -5507,9 +5546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -5572,6 +5611,17 @@
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
@@ -5733,9 +5783,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
@@ -6589,6 +6639,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
@@ -7202,9 +7258,9 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.32.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.4.tgz",
|
||||
"integrity": "sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg==",
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
@@ -7833,7 +7889,7 @@
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
@@ -8193,16 +8249,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wait-on": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
|
||||
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
|
||||
"integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"joi": "^17.7.0",
|
||||
"axios": "^1.6.1",
|
||||
"joi": "^17.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.7",
|
||||
"rxjs": "^7.8.0"
|
||||
"minimist": "^1.2.8",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"wait-on": "bin/wait-on"
|
||||
@@ -8211,6 +8267,17 @@
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wait-on/node_modules/axios": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
@@ -8235,7 +8302,7 @@
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
@@ -8305,7 +8372,7 @@
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
@@ -8951,6 +9018,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@nestjs/cache-manager": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/cache-manager/-/cache-manager-2.1.0.tgz",
|
||||
"integrity": "sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@nestjs/cli": {
|
||||
"version": "10.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.10.tgz",
|
||||
@@ -9393,24 +9466,13 @@
|
||||
}
|
||||
},
|
||||
"@types/clamscan": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.4.tgz",
|
||||
"integrity": "sha512-NpD+EmE+ZK5WRJOAmeDuSYJIv15BUnc4PxQA+m3QNkutaPBZ7bmLDTvqBu2iDchs7YKQjiEQEwEMvsdwtdtImA==",
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.7.tgz",
|
||||
"integrity": "sha512-YopQF+D1pqbvMbvqTQx2MdxEqFbQAiPJKtjj/KnK9xxgmYkdt3dInkj/k6a+bztT4TexQ+tCUi/5D3LqDZIdhg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"axios": "^0.24.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
@@ -9542,6 +9604,16 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.5.tgz",
|
||||
"integrity": "sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg=="
|
||||
},
|
||||
"@types/node-fetch": {
|
||||
"version": "2.6.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node-fetch/-/node-fetch-2.6.6.tgz",
|
||||
"integrity": "sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"form-data": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/nodemailer": {
|
||||
"version": "6.4.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.9.tgz",
|
||||
@@ -10185,13 +10257,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
|
||||
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
|
||||
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.9",
|
||||
"form-data": "^4.0.0"
|
||||
"follow-redirects": "^1.14.4"
|
||||
}
|
||||
},
|
||||
"b4a": {
|
||||
@@ -10386,6 +10457,22 @@
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="
|
||||
},
|
||||
"cache-manager": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/cache-manager/-/cache-manager-5.2.4.tgz",
|
||||
"integrity": "sha512-gkuCjug16NdGvKm/sydxGVx17uffrSWcEe2xraBtwRCgdYcFxwJAla4OYpASAZT2yhSoxgDiWL9XH6IAChcZJA==",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lru-cache": "^10.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.0.1.tgz",
|
||||
"integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
|
||||
@@ -12207,15 +12294,15 @@
|
||||
}
|
||||
},
|
||||
"joi": {
|
||||
"version": "17.7.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.7.0.tgz",
|
||||
"integrity": "sha512-1/ugc8djfn93rTE3WRKdCzGGt/EtiYKxITMO4Wiv6q5JL1gl9ePt4kBsl1S499nbosspfctIQTpYIhSmHA3WAg==",
|
||||
"version": "17.11.0",
|
||||
"resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz",
|
||||
"integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@hapi/hoek": "^9.0.0",
|
||||
"@hapi/topo": "^5.0.0",
|
||||
"@sideway/address": "^4.1.3",
|
||||
"@sideway/formula": "^3.0.0",
|
||||
"@sideway/formula": "^3.0.1",
|
||||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@@ -12394,6 +12481,11 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||
},
|
||||
"lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
@@ -12586,9 +12678,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
|
||||
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
|
||||
},
|
||||
"minipass": {
|
||||
"version": "3.3.4",
|
||||
@@ -12636,6 +12728,11 @@
|
||||
"integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
|
||||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
@@ -12767,9 +12864,9 @@
|
||||
}
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
@@ -13388,6 +13485,12 @@
|
||||
"ipaddr.js": "1.9.1"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
@@ -13845,9 +13948,9 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.32.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.4.tgz",
|
||||
"integrity": "sha512-exUnZewqVZC6UXqXuQ8fyJJv0M968feBi04jb9GcUHrWtkRoAKnbJt8IfwT4NJs7FskArbJ14JAFGVuooszoGg==",
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||
"requires": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.2",
|
||||
@@ -14306,7 +14409,7 @@
|
||||
},
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"tree-kill": {
|
||||
@@ -14553,16 +14656,29 @@
|
||||
}
|
||||
},
|
||||
"wait-on": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz",
|
||||
"integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
|
||||
"integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"axios": "^0.27.2",
|
||||
"joi": "^17.7.0",
|
||||
"axios": "^1.6.1",
|
||||
"joi": "^17.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.7",
|
||||
"rxjs": "^7.8.0"
|
||||
"minimist": "^1.2.8",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
|
||||
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
@@ -14586,7 +14702,7 @@
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"webpack": {
|
||||
@@ -14635,7 +14751,7 @@
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.18.2",
|
||||
"version": "0.21.4",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
@@ -13,6 +13,7 @@
|
||||
"seed": "ts-node prisma/seed/config.seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.1.0",
|
||||
"@nestjs/common": "^10.1.2",
|
||||
"@nestjs/config": "^3.0.0",
|
||||
"@nestjs/core": "^10.1.2",
|
||||
@@ -26,6 +27,7 @@
|
||||
"archiver": "^5.3.1",
|
||||
"argon2": "^0.30.3",
|
||||
"body-parser": "^1.20.2",
|
||||
"cache-manager": "^5.2.4",
|
||||
"clamscan": "^2.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
@@ -33,6 +35,8 @@
|
||||
"cookie-parser": "^1.4.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.29.4",
|
||||
"nanoid": "^3.3.6",
|
||||
"node-fetch": "^2.7.0",
|
||||
"nodemailer": "^6.9.4",
|
||||
"otplib": "^12.0.1",
|
||||
"passport": "^0.6.0",
|
||||
@@ -57,6 +61,7 @@
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.4.5",
|
||||
"@types/node-fetch": "^2.6.6",
|
||||
"@types/nodemailer": "^6.4.9",
|
||||
"@types/passport-jwt": "^3.0.9",
|
||||
"@types/qrcode-svg": "^1.1.1",
|
||||
|
||||
31
backend/prisma/migrations/20231021165436_oauth/migration.sql
Normal file
31
backend/prisma/migrations/20231021165436_oauth/migration.sql
Normal file
@@ -0,0 +1,31 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "OAuthUser" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"provider" TEXT NOT NULL,
|
||||
"providerUserId" TEXT NOT NULL,
|
||||
"providerUsername" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
CONSTRAINT "OAuthUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT,
|
||||
"isAdmin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"totpEnabled" BOOLEAN NOT NULL DEFAULT false,
|
||||
"totpVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"totpSecret" TEXT
|
||||
);
|
||||
INSERT INTO "new_User" ("createdAt", "email", "id", "isAdmin", "password", "totpEnabled", "totpSecret", "totpVerified", "updatedAt", "username") SELECT "createdAt", "email", "id", "isAdmin", "password", "totpEnabled", "totpSecret", "totpVerified", "updatedAt", "username" FROM "User";
|
||||
DROP TABLE "User";
|
||||
ALTER TABLE "new_User" RENAME TO "User";
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -14,7 +14,7 @@ model User {
|
||||
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
password String?
|
||||
isAdmin Boolean @default(false)
|
||||
|
||||
shares Share[]
|
||||
@@ -26,6 +26,8 @@ model User {
|
||||
totpVerified Boolean @default(false)
|
||||
totpSecret String?
|
||||
resetPasswordToken ResetPasswordToken?
|
||||
|
||||
oAuthUsers OAuthUser[]
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
@@ -60,6 +62,15 @@ model ResetPasswordToken {
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model OAuthUser {
|
||||
id String @id @default(uuid())
|
||||
provider String
|
||||
providerUserId String
|
||||
providerUsername String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Share {
|
||||
id String @id @default(uuid())
|
||||
createdAt DateTime @default(now())
|
||||
@@ -134,7 +145,7 @@ model Config {
|
||||
name String
|
||||
category String
|
||||
type String
|
||||
defaultValue String @default("")
|
||||
defaultValue String @default("")
|
||||
value String?
|
||||
obscured Boolean @default(false)
|
||||
secret Boolean @default(true)
|
||||
|
||||
@@ -5,7 +5,7 @@ const configVariables: ConfigVariables = {
|
||||
internal: {
|
||||
jwtSecret: {
|
||||
type: "string",
|
||||
defaultValue: crypto.randomBytes(256).toString("base64"),
|
||||
value: crypto.randomBytes(256).toString("base64"),
|
||||
locked: true,
|
||||
},
|
||||
},
|
||||
@@ -37,6 +37,11 @@ const configVariables: ConfigVariables = {
|
||||
defaultValue: "false",
|
||||
secret: false,
|
||||
},
|
||||
maxExpiration: {
|
||||
type: "number",
|
||||
defaultValue: "0",
|
||||
secret: false,
|
||||
},
|
||||
maxSize: {
|
||||
type: "number",
|
||||
defaultValue: "1000000000",
|
||||
@@ -119,6 +124,97 @@ const configVariables: ConfigVariables = {
|
||||
obscured: true,
|
||||
},
|
||||
},
|
||||
oauth: {
|
||||
"allowRegistration": {
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
},
|
||||
"ignoreTotp": {
|
||||
type: "boolean",
|
||||
defaultValue: "true",
|
||||
},
|
||||
"github-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"github-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"github-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
"google-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"google-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"google-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
"microsoft-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"microsoft-tenant": {
|
||||
type: "string",
|
||||
defaultValue: "common",
|
||||
},
|
||||
"microsoft-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"microsoft-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
"discord-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"discord-limitedGuild": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"discord-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"discord-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
"oidc-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"oidc-discoveryUri": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-usernameClaim": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"oidc-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
type ConfigVariables = {
|
||||
@@ -174,7 +270,7 @@ async function migrateConfigVariables() {
|
||||
for (const existingConfigVariable of existingConfigVariables) {
|
||||
const configVariable =
|
||||
configVariables[existingConfigVariable.category]?.[
|
||||
existingConfigVariable.name
|
||||
existingConfigVariable.name
|
||||
];
|
||||
if (!configVariable) {
|
||||
await prisma.config.delete({
|
||||
|
||||
@@ -15,6 +15,8 @@ import { UserModule } from "./user/user.module";
|
||||
import { ClamScanModule } from "./clamscan/clamscan.module";
|
||||
import { ReverseShareModule } from "./reverseShare/reverseShare.module";
|
||||
import { AppController } from "./app.controller";
|
||||
import { OAuthModule } from "./oauth/oauth.module";
|
||||
import { CacheModule } from "@nestjs/cache-manager";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -33,10 +35,12 @@ import { AppController } from "./app.controller";
|
||||
ScheduleModule.forRoot(),
|
||||
ClamScanModule,
|
||||
ReverseShareModule,
|
||||
OAuthModule,
|
||||
CacheModule.register({
|
||||
isGlobal: true,
|
||||
}),
|
||||
],
|
||||
controllers:[
|
||||
AppController,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
|
||||
@@ -47,7 +47,7 @@ export class AuthController {
|
||||
|
||||
const result = await this.authService.signUp(dto);
|
||||
|
||||
response = this.addTokensToResponse(
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
result.refreshToken,
|
||||
result.accessToken,
|
||||
@@ -66,7 +66,7 @@ export class AuthController {
|
||||
const result = await this.authService.signIn(dto);
|
||||
|
||||
if (result.accessToken && result.refreshToken) {
|
||||
response = this.addTokensToResponse(
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
result.refreshToken,
|
||||
result.accessToken,
|
||||
@@ -85,7 +85,7 @@ export class AuthController {
|
||||
) {
|
||||
const result = await this.authTotpService.signInTotp(dto);
|
||||
|
||||
response = this.addTokensToResponse(
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
result.refreshToken,
|
||||
result.accessToken,
|
||||
@@ -117,11 +117,11 @@ export class AuthController {
|
||||
) {
|
||||
const result = await this.authService.updatePassword(
|
||||
user,
|
||||
dto.oldPassword,
|
||||
dto.password,
|
||||
dto.oldPassword,
|
||||
);
|
||||
|
||||
response = this.addTokensToResponse(response, result.refreshToken);
|
||||
this.authService.addTokensToResponse(response, result.refreshToken);
|
||||
return new TokenDTO().from(result);
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export class AuthController {
|
||||
const accessToken = await this.authService.refreshAccessToken(
|
||||
request.cookies.refresh_token,
|
||||
);
|
||||
response = this.addTokensToResponse(response, undefined, accessToken);
|
||||
this.authService.addTokensToResponse(response, undefined, accessToken);
|
||||
return new TokenDTO().from({ accessToken });
|
||||
}
|
||||
|
||||
@@ -172,22 +172,4 @@ export class AuthController {
|
||||
// Note: We use VerifyTotpDTO here because it has both fields we need: password and totp code
|
||||
return this.authTotpService.disableTotp(user, body.password, body.code);
|
||||
}
|
||||
|
||||
private addTokensToResponse(
|
||||
response: Response,
|
||||
refreshToken?: string,
|
||||
accessToken?: string,
|
||||
) {
|
||||
if (accessToken)
|
||||
response.cookie("access_token", accessToken, { sameSite: "lax" });
|
||||
if (refreshToken)
|
||||
response.cookie("refresh_token", refreshToken, {
|
||||
path: "/api/auth/token",
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,12 @@ import { AuthTotpService } from "./authTotp.service";
|
||||
import { JwtStrategy } from "./strategy/jwt.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [JwtModule.register({}), EmailModule],
|
||||
imports: [
|
||||
JwtModule.register({
|
||||
global: true,
|
||||
}),
|
||||
EmailModule,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, AuthTotpService, JwtStrategy],
|
||||
exports: [AuthService],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { JwtService } from "@nestjs/jwt";
|
||||
import { User } from "@prisma/client";
|
||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import * as argon from "argon2";
|
||||
import { Request, Response } from "express";
|
||||
import * as moment from "moment";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { EmailService } from "src/email/email.service";
|
||||
@@ -27,7 +28,7 @@ export class AuthService {
|
||||
async signUp(dto: AuthRegisterDTO) {
|
||||
const isFirstUser = (await this.prisma.user.count()) == 0;
|
||||
|
||||
const hash = await argon.hash(dto.password);
|
||||
const hash = dto.password ? await argon.hash(dto.password) : null;
|
||||
try {
|
||||
const user = await this.prisma.user.create({
|
||||
data: {
|
||||
@@ -43,7 +44,7 @@ export class AuthService {
|
||||
);
|
||||
const accessToken = await this.createAccessToken(user, refreshTokenId);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
return { accessToken, refreshToken, user };
|
||||
} catch (e) {
|
||||
if (e instanceof PrismaClientKnownRequestError) {
|
||||
if (e.code == "P2002") {
|
||||
@@ -69,9 +70,16 @@ export class AuthService {
|
||||
if (!user || !(await argon.verify(user.password, dto.password)))
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
|
||||
return this.generateToken(user);
|
||||
}
|
||||
|
||||
async generateToken(user: User, isOAuth = false) {
|
||||
// TODO: Make all old loginTokens invalid when a new one is created
|
||||
// Check if the user has TOTP enabled
|
||||
if (user.totpVerified) {
|
||||
if (
|
||||
user.totpVerified &&
|
||||
!(isOAuth && this.config.get("oauth.ignoreTotp"))
|
||||
) {
|
||||
const loginToken = await this.createLoginToken(user.id);
|
||||
|
||||
return { loginToken };
|
||||
@@ -129,9 +137,11 @@ export class AuthService {
|
||||
});
|
||||
}
|
||||
|
||||
async updatePassword(user: User, oldPassword: string, newPassword: string) {
|
||||
if (!(await argon.verify(user.password, oldPassword)))
|
||||
throw new ForbiddenException("Invalid password");
|
||||
async updatePassword(user: User, newPassword: string, oldPassword?: string) {
|
||||
const isPasswordValid =
|
||||
!user.password || !(await argon.verify(user.password, oldPassword));
|
||||
|
||||
if (!isPasswordValid) throw new ForbiddenException("Invalid password");
|
||||
|
||||
const hash = await argon.hash(newPassword);
|
||||
|
||||
@@ -210,4 +220,38 @@ export class AuthService {
|
||||
|
||||
return loginToken;
|
||||
}
|
||||
|
||||
addTokensToResponse(
|
||||
response: Response,
|
||||
refreshToken?: string,
|
||||
accessToken?: string,
|
||||
) {
|
||||
if (accessToken)
|
||||
response.cookie("access_token", accessToken, { sameSite: "lax" });
|
||||
if (refreshToken)
|
||||
response.cookie("refresh_token", refreshToken, {
|
||||
path: "/api/auth/token",
|
||||
httpOnly: true,
|
||||
sameSite: "strict",
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user id if the user is logged in, null otherwise
|
||||
*/
|
||||
async getIdOfCurrentUser(request: Request): Promise<string | null> {
|
||||
if (!request.cookies.access_token) return null;
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(
|
||||
request.cookies.access_token,
|
||||
{
|
||||
secret: this.config.get("internal.jwtSecret"),
|
||||
},
|
||||
);
|
||||
return payload.sub;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ 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";
|
||||
@@ -18,47 +17,32 @@ export class AuthTotpService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private authService: AuthService,
|
||||
private config: ConfigService,
|
||||
) {}
|
||||
|
||||
async signInTotp(dto: AuthSignInTotpDTO) {
|
||||
if (!dto.email && !dto.username)
|
||||
throw new BadRequestException("Email or username is required");
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email: dto.email }, { username: dto.username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !(await argon.verify(user.password, dto.password)))
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
|
||||
const token = await this.prisma.loginToken.findFirst({
|
||||
where: {
|
||||
token: dto.loginToken,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!token || token.userId != user.id || token.used)
|
||||
if (!token || token.used)
|
||||
throw new UnauthorizedException("Invalid login token");
|
||||
|
||||
if (token.expiresAt < new Date())
|
||||
throw new UnauthorizedException("Login token expired", "token_expired");
|
||||
|
||||
// Check the TOTP code
|
||||
const { totpSecret } = await this.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
select: { totpSecret: true },
|
||||
});
|
||||
const { totpSecret } = token.user;
|
||||
|
||||
if (!totpSecret) {
|
||||
throw new BadRequestException("TOTP is not enabled");
|
||||
}
|
||||
|
||||
const expected = authenticator.generate(totpSecret);
|
||||
|
||||
if (dto.totp !== expected) {
|
||||
if (!authenticator.check(dto.totp, totpSecret)) {
|
||||
throw new BadRequestException("Invalid code");
|
||||
}
|
||||
|
||||
@@ -69,9 +53,9 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
const { refreshToken, refreshTokenId } =
|
||||
await this.authService.createRefreshToken(user.id);
|
||||
await this.authService.createRefreshToken(token.user.id);
|
||||
const accessToken = await this.authService.createAccessToken(
|
||||
user,
|
||||
token.user,
|
||||
refreshTokenId,
|
||||
);
|
||||
|
||||
@@ -92,12 +76,11 @@ export class AuthTotpService {
|
||||
throw new BadRequestException("TOTP is already enabled");
|
||||
}
|
||||
|
||||
// TODO: Maybe make the issuer configurable with env vars?
|
||||
const secret = authenticator.generateSecret();
|
||||
|
||||
const otpURL = totp.keyuri(
|
||||
user.username || user.email,
|
||||
this.config.get("general.appName"),
|
||||
"pingvin-share",
|
||||
secret,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IsString } from "class-validator";
|
||||
import { AuthSignInDTO } from "./authSignIn.dto";
|
||||
|
||||
export class AuthSignInTotpDTO extends AuthSignInDTO {
|
||||
export class AuthSignInTotpDTO {
|
||||
@IsString()
|
||||
totp: string;
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { IsString } from "class-validator";
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { UserDTO } from "src/user/dto/user.dto";
|
||||
|
||||
export class UpdatePasswordDTO extends PickType(UserDTO, ["password"]) {
|
||||
@IsString()
|
||||
oldPassword: string;
|
||||
@IsOptional()
|
||||
oldPassword?: string;
|
||||
}
|
||||
|
||||
@@ -6,13 +6,20 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { Config } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
/**
|
||||
* ConfigService extends EventEmitter to allow listening for config updates,
|
||||
* now only `update` event will be emitted.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ConfigService {
|
||||
export class ConfigService extends EventEmitter {
|
||||
constructor(
|
||||
@Inject("CONFIG_VARIABLES") private configVariables: Config[],
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
get(key: `${string}.${string}`): any {
|
||||
const configVariable = this.configVariables.filter(
|
||||
@@ -105,6 +112,8 @@ export class ConfigService {
|
||||
|
||||
this.configVariables = await this.prisma.config.findMany();
|
||||
|
||||
this.emit("update", key, value);
|
||||
|
||||
return updatedVariable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export class LogoService {
|
||||
fs.promises.writeFile(
|
||||
`${IMAGES_PATH}/icons/icon-${size}x${size}.png`,
|
||||
resized,
|
||||
"binary"
|
||||
"binary",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
@@ -81,4 +82,14 @@ export class FileController {
|
||||
|
||||
return new StreamableFile(file.file);
|
||||
}
|
||||
|
||||
@Delete(":fileId")
|
||||
@SkipThrottle()
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async remove(
|
||||
@Param("fileId") fileId: string,
|
||||
@Param("shareId") shareId: string,
|
||||
) {
|
||||
await this.fileService.remove(shareId, fileId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,18 @@ export class FileService {
|
||||
};
|
||||
}
|
||||
|
||||
async remove(shareId: string, fileId: string) {
|
||||
const fileMetaData = await this.prisma.file.findUnique({
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
|
||||
fs.unlinkSync(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||
|
||||
await this.prisma.file.delete({ where: { id: fileId } });
|
||||
}
|
||||
|
||||
async deleteAllFiles(shareId: string) {
|
||||
await fs.promises.rm(`${SHARE_DIRECTORY}/${shareId}`, {
|
||||
recursive: true,
|
||||
|
||||
@@ -61,6 +61,28 @@ export class JobsService {
|
||||
}
|
||||
}
|
||||
|
||||
@Cron("0 */6 * * *")
|
||||
async deleteUnfinishedShares() {
|
||||
const unfinishedShares = await this.prisma.share.findMany({
|
||||
where: {
|
||||
createdAt: { lt: moment().subtract(1, "day").toDate() },
|
||||
uploadLocked: false,
|
||||
},
|
||||
});
|
||||
|
||||
for (const unfinishedShare of unfinishedShares) {
|
||||
await this.prisma.share.delete({
|
||||
where: { id: unfinishedShare.id },
|
||||
});
|
||||
|
||||
await this.fileService.deleteAllFiles(unfinishedShare.id);
|
||||
}
|
||||
|
||||
if (unfinishedShares.length > 0) {
|
||||
this.logger.log(`Deleted ${unfinishedShares.length} unfinished shares`);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron("0 0 * * *")
|
||||
deleteTemporaryFiles() {
|
||||
let filesDeleted = 0;
|
||||
@@ -93,7 +115,7 @@ export class JobsService {
|
||||
this.logger.log(`Deleted ${filesDeleted} temporary files`);
|
||||
}
|
||||
|
||||
@Cron("0 * * * *")
|
||||
@Cron("1 * * * *")
|
||||
async deleteExpiredTokens() {
|
||||
const { count: refreshTokenCount } =
|
||||
await this.prisma.refreshToken.deleteMany({
|
||||
|
||||
9
backend/src/oauth/dto/oauthCallback.dto.ts
Normal file
9
backend/src/oauth/dto/oauthCallback.dto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class OAuthCallbackDto {
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@IsString()
|
||||
state: string;
|
||||
}
|
||||
6
backend/src/oauth/dto/oauthSignIn.dto.ts
Normal file
6
backend/src/oauth/dto/oauthSignIn.dto.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface OAuthSignInDto {
|
||||
provider: "github" | "google" | "microsoft" | "discord" | "oidc";
|
||||
providerId: string;
|
||||
providerUsername: string;
|
||||
email: string;
|
||||
}
|
||||
15
backend/src/oauth/exceptions/errorPage.exception.ts
Normal file
15
backend/src/oauth/exceptions/errorPage.exception.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export class ErrorPageException extends Error {
|
||||
/**
|
||||
* Exception for redirecting to error page (all i18n key should omit `error.msg` and `error.param` prefix)
|
||||
* @param key i18n key of message
|
||||
* @param redirect redirect url
|
||||
* @param params message params (key)
|
||||
*/
|
||||
constructor(
|
||||
public readonly key: string = "default",
|
||||
public readonly redirect?: string,
|
||||
public readonly params?: string[],
|
||||
) {
|
||||
super("error");
|
||||
}
|
||||
}
|
||||
39
backend/src/oauth/filter/errorPageException.filter.ts
Normal file
39
backend/src/oauth/filter/errorPageException.filter.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { ErrorPageException } from "../exceptions/errorPage.exception";
|
||||
|
||||
@Catch(ErrorPageException)
|
||||
export class ErrorPageExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger(ErrorPageExceptionFilter.name);
|
||||
|
||||
constructor(private config: ConfigService) {}
|
||||
|
||||
catch(exception: ErrorPageException, host: ArgumentsHost) {
|
||||
this.logger.error(
|
||||
JSON.stringify({
|
||||
error: exception.key,
|
||||
params: exception.params,
|
||||
redirect: exception.redirect,
|
||||
}),
|
||||
);
|
||||
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
|
||||
const url = new URL(`${this.config.get("general.appUrl")}/error`);
|
||||
url.searchParams.set("error", exception.key);
|
||||
if (exception.redirect) {
|
||||
url.searchParams.set("redirect", exception.redirect);
|
||||
} else {
|
||||
const redirect = ctx.getRequest().cookies.access_token
|
||||
? "/account"
|
||||
: "/auth/signIn";
|
||||
url.searchParams.set("redirect", redirect);
|
||||
}
|
||||
if (exception.params) {
|
||||
url.searchParams.set("params", exception.params.join(","));
|
||||
}
|
||||
|
||||
response.redirect(url.toString());
|
||||
}
|
||||
}
|
||||
38
backend/src/oauth/filter/oauthException.filter.ts
Normal file
38
backend/src/oauth/filter/oauthException.filter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
ArgumentsHost,
|
||||
Catch,
|
||||
ExceptionFilter,
|
||||
HttpException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
|
||||
@Catch(HttpException)
|
||||
export class OAuthExceptionFilter implements ExceptionFilter {
|
||||
private errorKeys: Record<string, string> = {
|
||||
access_denied: "access_denied",
|
||||
expired_token: "expired_token",
|
||||
};
|
||||
private readonly logger = new Logger(OAuthExceptionFilter.name);
|
||||
|
||||
constructor(private config: ConfigService) {}
|
||||
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
|
||||
this.logger.error(exception.message);
|
||||
this.logger.error(
|
||||
"Request query: " + JSON.stringify(request.query, null, 2),
|
||||
);
|
||||
|
||||
const key = this.errorKeys[request.query.error] || "default";
|
||||
|
||||
const url = new URL(`${this.config.get("general.appUrl")}/error`);
|
||||
url.searchParams.set("redirect", "/account");
|
||||
url.searchParams.set("error", key);
|
||||
|
||||
response.redirect(url.toString());
|
||||
}
|
||||
}
|
||||
12
backend/src/oauth/guard/oauth.guard.ts
Normal file
12
backend/src/oauth/guard/oauth.guard.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthGuard implements CanActivate {
|
||||
constructor() {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const provider = request.params.provider;
|
||||
return request.query.state === request.cookies[`oauth_${provider}_state`];
|
||||
}
|
||||
}
|
||||
24
backend/src/oauth/guard/provider.guard.ts
Normal file
24
backend/src/oauth/guard/provider.guard.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Inject,
|
||||
Injectable,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class ProviderGuard implements CanActivate {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
@Inject("OAUTH_PLATFORMS") private platforms: string[],
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const provider = request.params.provider;
|
||||
return (
|
||||
this.platforms.includes(provider) &&
|
||||
this.config.get(`oauth.${provider}-enabled`)
|
||||
);
|
||||
}
|
||||
}
|
||||
110
backend/src/oauth/oauth.controller.ts
Normal file
110
backend/src/oauth/oauth.controller.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Inject,
|
||||
Param,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
Res,
|
||||
UseFilters,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request, Response } from "express";
|
||||
import { nanoid } from "nanoid";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { GetUser } from "../auth/decorator/getUser.decorator";
|
||||
import { JwtGuard } from "../auth/guard/jwt.guard";
|
||||
import { ConfigService } from "../config/config.service";
|
||||
import { OAuthCallbackDto } from "./dto/oauthCallback.dto";
|
||||
import { ErrorPageExceptionFilter } from "./filter/errorPageException.filter";
|
||||
import { OAuthGuard } from "./guard/oauth.guard";
|
||||
import { ProviderGuard } from "./guard/provider.guard";
|
||||
import { OAuthService } from "./oauth.service";
|
||||
import { OAuthProvider } from "./provider/oauthProvider.interface";
|
||||
import { OAuthExceptionFilter } from "./filter/oauthException.filter";
|
||||
|
||||
@Controller("oauth")
|
||||
export class OAuthController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private oauthService: OAuthService,
|
||||
private config: ConfigService,
|
||||
@Inject("OAUTH_PROVIDERS")
|
||||
private providers: Record<string, OAuthProvider<unknown>>,
|
||||
) {}
|
||||
|
||||
@Get("available")
|
||||
available() {
|
||||
return this.oauthService.available();
|
||||
}
|
||||
|
||||
@Get("status")
|
||||
@UseGuards(JwtGuard)
|
||||
async status(@GetUser() user: User) {
|
||||
return this.oauthService.status(user);
|
||||
}
|
||||
|
||||
@Get("auth/:provider")
|
||||
@UseGuards(ProviderGuard)
|
||||
@UseFilters(ErrorPageExceptionFilter)
|
||||
async auth(
|
||||
@Param("provider") provider: string,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
const state = nanoid(16);
|
||||
const url = await this.providers[provider].getAuthEndpoint(state);
|
||||
response.cookie(`oauth_${provider}_state`, state, { sameSite: "lax" });
|
||||
response.redirect(url);
|
||||
}
|
||||
|
||||
@Get("callback/:provider")
|
||||
@UseGuards(ProviderGuard, OAuthGuard)
|
||||
@UseFilters(ErrorPageExceptionFilter, OAuthExceptionFilter)
|
||||
async callback(
|
||||
@Param("provider") provider: string,
|
||||
@Query() query: OAuthCallbackDto,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
const oauthToken = await this.providers[provider].getToken(query);
|
||||
const user = await this.providers[provider].getUserInfo(oauthToken, query);
|
||||
const id = await this.authService.getIdOfCurrentUser(request);
|
||||
|
||||
if (id) {
|
||||
await this.oauthService.link(
|
||||
id,
|
||||
provider,
|
||||
user.providerId,
|
||||
user.providerUsername,
|
||||
);
|
||||
response.redirect(this.config.get("general.appUrl") + "/account");
|
||||
} else {
|
||||
const token: {
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
loginToken?: string;
|
||||
} = await this.oauthService.signIn(user);
|
||||
if (token.accessToken) {
|
||||
this.authService.addTokensToResponse(
|
||||
response,
|
||||
token.refreshToken,
|
||||
token.accessToken,
|
||||
);
|
||||
response.redirect(this.config.get("general.appUrl"));
|
||||
} else {
|
||||
response.redirect(
|
||||
this.config.get("general.appUrl") + `/auth/totp/${token.loginToken}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Post("unlink/:provider")
|
||||
@UseGuards(JwtGuard, ProviderGuard)
|
||||
@UseFilters(ErrorPageExceptionFilter)
|
||||
unlink(@GetUser() user: User, @Param("provider") provider: string) {
|
||||
return this.oauthService.unlink(user, provider);
|
||||
}
|
||||
}
|
||||
56
backend/src/oauth/oauth.module.ts
Normal file
56
backend/src/oauth/oauth.module.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { OAuthController } from "./oauth.controller";
|
||||
import { OAuthService } from "./oauth.service";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
import { GitHubProvider } from "./provider/github.provider";
|
||||
import { GoogleProvider } from "./provider/google.provider";
|
||||
import { OAuthProvider } from "./provider/oauthProvider.interface";
|
||||
import { OidcProvider } from "./provider/oidc.provider";
|
||||
import { DiscordProvider } from "./provider/discord.provider";
|
||||
import { MicrosoftProvider } from "./provider/microsoft.provider";
|
||||
|
||||
@Module({
|
||||
controllers: [OAuthController],
|
||||
providers: [
|
||||
OAuthService,
|
||||
GitHubProvider,
|
||||
GoogleProvider,
|
||||
MicrosoftProvider,
|
||||
DiscordProvider,
|
||||
OidcProvider,
|
||||
{
|
||||
provide: "OAUTH_PROVIDERS",
|
||||
useFactory(
|
||||
github: GitHubProvider,
|
||||
google: GoogleProvider,
|
||||
microsoft: MicrosoftProvider,
|
||||
discord: DiscordProvider,
|
||||
oidc: OidcProvider,
|
||||
): Record<string, OAuthProvider<unknown>> {
|
||||
return {
|
||||
github,
|
||||
google,
|
||||
microsoft,
|
||||
discord,
|
||||
oidc,
|
||||
};
|
||||
},
|
||||
inject: [
|
||||
GitHubProvider,
|
||||
GoogleProvider,
|
||||
MicrosoftProvider,
|
||||
DiscordProvider,
|
||||
OidcProvider,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: "OAUTH_PLATFORMS",
|
||||
useFactory(providers: Record<string, OAuthProvider<unknown>>): string[] {
|
||||
return Object.keys(providers);
|
||||
},
|
||||
inject: ["OAUTH_PROVIDERS"],
|
||||
},
|
||||
],
|
||||
imports: [AuthModule],
|
||||
})
|
||||
export class OAuthModule {}
|
||||
171
backend/src/oauth/oauth.service.ts
Normal file
171
backend/src/oauth/oauth.service.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { nanoid } from "nanoid";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { ConfigService } from "../config/config.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { OAuthSignInDto } from "./dto/oauthSignIn.dto";
|
||||
import { ErrorPageException } from "./exceptions/errorPage.exception";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private config: ConfigService,
|
||||
private auth: AuthService,
|
||||
@Inject("OAUTH_PLATFORMS") private platforms: string[],
|
||||
) {}
|
||||
|
||||
available(): string[] {
|
||||
return this.platforms
|
||||
.map((platform) => [
|
||||
platform,
|
||||
this.config.get(`oauth.${platform}-enabled`),
|
||||
])
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([platform, _]) => platform);
|
||||
}
|
||||
|
||||
async status(user: User) {
|
||||
const oauthUsers = await this.prisma.oAuthUser.findMany({
|
||||
select: {
|
||||
provider: true,
|
||||
providerUsername: true,
|
||||
},
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
return Object.fromEntries(oauthUsers.map((u) => [u.provider, u]));
|
||||
}
|
||||
|
||||
async signIn(user: OAuthSignInDto) {
|
||||
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
||||
where: {
|
||||
provider: user.provider,
|
||||
providerUserId: user.providerId,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
if (oauthUser) {
|
||||
return this.auth.generateToken(oauthUser.user, true);
|
||||
}
|
||||
|
||||
return this.signUp(user);
|
||||
}
|
||||
|
||||
async link(
|
||||
userId: string,
|
||||
provider: string,
|
||||
providerUserId: string,
|
||||
providerUsername: string,
|
||||
) {
|
||||
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
||||
where: {
|
||||
provider,
|
||||
providerUserId,
|
||||
},
|
||||
});
|
||||
if (oauthUser) {
|
||||
throw new ErrorPageException("already_linked", "/account", [
|
||||
`provider_${provider}`,
|
||||
]);
|
||||
}
|
||||
|
||||
await this.prisma.oAuthUser.create({
|
||||
data: {
|
||||
userId,
|
||||
provider,
|
||||
providerUsername,
|
||||
providerUserId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async unlink(user: User, provider: string) {
|
||||
const oauthUser = await this.prisma.oAuthUser.findFirst({
|
||||
where: {
|
||||
userId: user.id,
|
||||
provider,
|
||||
},
|
||||
});
|
||||
if (oauthUser) {
|
||||
await this.prisma.oAuthUser.delete({
|
||||
where: {
|
||||
id: oauthUser.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new ErrorPageException("not_linked", "/account", [provider]);
|
||||
}
|
||||
}
|
||||
|
||||
private async getAvailableUsername(email: string) {
|
||||
// only remove + and - from email for now (maybe not enough)
|
||||
let username = email.split("@")[0].replace(/[+-]/g, "").substring(0, 20);
|
||||
while (true) {
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
username: username,
|
||||
},
|
||||
});
|
||||
if (user) {
|
||||
username = username + "_" + nanoid(10).replaceAll("-", "");
|
||||
} else {
|
||||
return username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async signUp(user: OAuthSignInDto) {
|
||||
// register
|
||||
if (!this.config.get("oauth.allowRegistration")) {
|
||||
throw new ErrorPageException("no_user", "/auth/signIn", [
|
||||
`provider_${user.provider}`,
|
||||
]);
|
||||
}
|
||||
|
||||
if (!user.email) {
|
||||
throw new ErrorPageException("no_email", "/auth/signIn", [
|
||||
`provider_${user.provider}`,
|
||||
]);
|
||||
}
|
||||
|
||||
const existingUser: User = await this.prisma.user.findFirst({
|
||||
where: {
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
await this.prisma.oAuthUser.create({
|
||||
data: {
|
||||
provider: user.provider,
|
||||
providerUserId: user.providerId.toString(),
|
||||
providerUsername: user.providerUsername,
|
||||
userId: existingUser.id,
|
||||
},
|
||||
});
|
||||
return this.auth.generateToken(existingUser, true);
|
||||
}
|
||||
|
||||
const result = await this.auth.signUp({
|
||||
email: user.email,
|
||||
username: await this.getAvailableUsername(user.email),
|
||||
password: null,
|
||||
});
|
||||
|
||||
await this.prisma.oAuthUser.create({
|
||||
data: {
|
||||
provider: user.provider,
|
||||
providerUserId: user.providerId.toString(),
|
||||
providerUsername: user.providerUsername,
|
||||
userId: result.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
136
backend/src/oauth/provider/discord.provider.ts
Normal file
136
backend/src/oauth/provider/discord.provider.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
import { ErrorPageException } from "../exceptions/errorPage.exception";
|
||||
import { OAuthProvider, OAuthToken } from "./oauthProvider.interface";
|
||||
@Injectable()
|
||||
export class DiscordProvider implements OAuthProvider<DiscordToken> {
|
||||
constructor(private config: ConfigService) {}
|
||||
|
||||
getAuthEndpoint(state: string): Promise<string> {
|
||||
let scope = "identify email";
|
||||
if (this.config.get("oauth.discord-limitedGuild")) {
|
||||
scope += " guilds";
|
||||
}
|
||||
return Promise.resolve(
|
||||
"https://discord.com/api/oauth2/authorize?" +
|
||||
new URLSearchParams({
|
||||
client_id: this.config.get("oauth.discord-clientId"),
|
||||
redirect_uri:
|
||||
this.config.get("general.appUrl") + "/api/oauth/callback/discord",
|
||||
response_type: "code",
|
||||
state,
|
||||
scope,
|
||||
}).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
private getAuthorizationHeader() {
|
||||
return (
|
||||
"Basic " +
|
||||
Buffer.from(
|
||||
this.config.get("oauth.discord-clientId") +
|
||||
":" +
|
||||
this.config.get("oauth.discord-clientSecret"),
|
||||
).toString("base64")
|
||||
);
|
||||
}
|
||||
|
||||
async getToken(query: OAuthCallbackDto): Promise<OAuthToken<DiscordToken>> {
|
||||
const res = await fetch("https://discord.com/api/v10/oauth2/token", {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: this.getAuthorizationHeader(),
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
code: query.code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri:
|
||||
this.config.get("general.appUrl") + "/api/oauth/callback/discord",
|
||||
}),
|
||||
});
|
||||
const token: DiscordToken = await res.json();
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
refreshToken: token.refresh_token,
|
||||
expiresIn: token.expires_in,
|
||||
scope: token.scope,
|
||||
tokenType: token.token_type,
|
||||
rawToken: token,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserInfo(token: OAuthToken<DiscordToken>): Promise<OAuthSignInDto> {
|
||||
const res = await fetch("https://discord.com/api/v10/users/@me", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `${token.tokenType || "Bearer"} ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
const user = (await res.json()) as DiscordUser;
|
||||
if (user.verified === false) {
|
||||
throw new ErrorPageException("unverified_account", undefined, [
|
||||
"provider_discord",
|
||||
]);
|
||||
}
|
||||
|
||||
const guild = this.config.get("oauth.discord-limitedGuild");
|
||||
if (guild) {
|
||||
await this.checkLimitedGuild(token, guild);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: "discord",
|
||||
providerId: user.id,
|
||||
providerUsername: user.global_name ?? user.username,
|
||||
email: user.email,
|
||||
};
|
||||
}
|
||||
|
||||
async checkLimitedGuild(token: OAuthToken<DiscordToken>, guildId: string) {
|
||||
try {
|
||||
const res = await fetch("https://discord.com/api/v10/users/@me/guilds", {
|
||||
method: "get",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `${token.tokenType || "Bearer"} ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
const guilds = (await res.json()) as DiscordPartialGuild[];
|
||||
if (!guilds.some((guild) => guild.id === guildId)) {
|
||||
throw new ErrorPageException("discord_guild_permission_denied");
|
||||
}
|
||||
} catch {
|
||||
throw new ErrorPageException("discord_guild_permission_denied");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DiscordToken {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface DiscordUser {
|
||||
id: string;
|
||||
username: string;
|
||||
global_name: string;
|
||||
email: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export interface DiscordPartialGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
owner: boolean;
|
||||
permissions: string;
|
||||
features: string[];
|
||||
}
|
||||
237
backend/src/oauth/provider/genericOidc.provider.ts
Normal file
237
backend/src/oauth/provider/genericOidc.provider.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Cache } from "cache-manager";
|
||||
import { nanoid } from "nanoid";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthProvider, OAuthToken } from "./oauthProvider.interface";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
import { ErrorPageException } from "../exceptions/errorPage.exception";
|
||||
|
||||
export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
protected discoveryUri: string;
|
||||
private configuration: OidcConfigurationCache;
|
||||
private jwk: OidcJwkCache;
|
||||
private logger: Logger = new Logger(
|
||||
Object.getPrototypeOf(this).constructor.name,
|
||||
);
|
||||
|
||||
protected constructor(
|
||||
protected name: string,
|
||||
protected keyOfConfigUpdateEvents: string[],
|
||||
protected config: ConfigService,
|
||||
protected jwtService: JwtService,
|
||||
protected cache: Cache,
|
||||
) {
|
||||
this.discoveryUri = this.getDiscoveryUri();
|
||||
this.config.addListener("update", (key: string, _: unknown) => {
|
||||
if (this.keyOfConfigUpdateEvents.includes(key)) {
|
||||
this.deinit();
|
||||
this.discoveryUri = this.getDiscoveryUri();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getRedirectUri(): string {
|
||||
return `${this.config.get("general.appUrl")}/api/oauth/callback/${
|
||||
this.name
|
||||
}`;
|
||||
}
|
||||
|
||||
async getConfiguration(): Promise<OidcConfiguration> {
|
||||
if (!this.configuration || this.configuration.expires < Date.now()) {
|
||||
await this.fetchConfiguration();
|
||||
}
|
||||
return this.configuration.data;
|
||||
}
|
||||
|
||||
async getJwk(): Promise<OidcJwk[]> {
|
||||
if (!this.jwk || this.jwk.expires < Date.now()) {
|
||||
await this.fetchJwk();
|
||||
}
|
||||
return this.jwk.data;
|
||||
}
|
||||
|
||||
async getAuthEndpoint(state: string) {
|
||||
const configuration = await this.getConfiguration();
|
||||
const endpoint = configuration.authorization_endpoint;
|
||||
|
||||
const nonce = nanoid();
|
||||
await this.cache.set(
|
||||
`oauth-${this.name}-nonce-${state}`,
|
||||
nonce,
|
||||
1000 * 60 * 5,
|
||||
);
|
||||
|
||||
return (
|
||||
endpoint +
|
||||
"?" +
|
||||
new URLSearchParams({
|
||||
client_id: this.config.get(`oauth.${this.name}-clientId`),
|
||||
response_type: "code",
|
||||
scope: "openid profile email",
|
||||
redirect_uri: this.getRedirectUri(),
|
||||
state,
|
||||
nonce,
|
||||
}).toString()
|
||||
);
|
||||
}
|
||||
|
||||
async getToken(query: OAuthCallbackDto): Promise<OAuthToken<OidcToken>> {
|
||||
const configuration = await this.getConfiguration();
|
||||
const endpoint = configuration.token_endpoint;
|
||||
const res = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
client_id: this.config.get(`oauth.${this.name}-clientId`),
|
||||
client_secret: this.config.get(`oauth.${this.name}-clientSecret`),
|
||||
grant_type: "authorization_code",
|
||||
code: query.code,
|
||||
redirect_uri: this.getRedirectUri(),
|
||||
}).toString(),
|
||||
});
|
||||
const token: OidcToken = await res.json();
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
expiresIn: token.expires_in,
|
||||
idToken: token.id_token,
|
||||
refreshToken: token.refresh_token,
|
||||
tokenType: token.token_type,
|
||||
rawToken: token,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserInfo(
|
||||
token: OAuthToken<OidcToken>,
|
||||
query: OAuthCallbackDto,
|
||||
claim?: string,
|
||||
): Promise<OAuthSignInDto> {
|
||||
const idTokenData = this.decodeIdToken(token.idToken);
|
||||
// maybe it's not necessary to verify the id token since it's directly obtained from the provider
|
||||
|
||||
const key = `oauth-${this.name}-nonce-${query.state}`;
|
||||
const nonce = await this.cache.get(key);
|
||||
await this.cache.del(key);
|
||||
if (nonce !== idTokenData.nonce) {
|
||||
this.logger.error(
|
||||
`Invalid nonce. Expected ${nonce}, but got ${idTokenData.nonce}`,
|
||||
);
|
||||
throw new ErrorPageException("invalid_token");
|
||||
}
|
||||
|
||||
const username = claim
|
||||
? idTokenData[claim]
|
||||
: idTokenData.name ||
|
||||
idTokenData.nickname ||
|
||||
idTokenData.preferred_username;
|
||||
|
||||
if (!username) {
|
||||
this.logger.error(
|
||||
`Can not get username from ID Token ${JSON.stringify(
|
||||
idTokenData,
|
||||
undefined,
|
||||
2,
|
||||
)}`,
|
||||
);
|
||||
throw new ErrorPageException("cannot_get_user_info", undefined, [
|
||||
`provider_${this.name}`,
|
||||
]);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: this.name as any,
|
||||
email: idTokenData.email,
|
||||
providerId: idTokenData.sub,
|
||||
providerUsername: username,
|
||||
};
|
||||
}
|
||||
|
||||
protected abstract getDiscoveryUri(): string;
|
||||
|
||||
private async fetchConfiguration(): Promise<void> {
|
||||
const res = await fetch(this.discoveryUri);
|
||||
const expires = res.headers.has("expires")
|
||||
? new Date(res.headers.get("expires")).getTime()
|
||||
: Date.now() + 1000 * 60 * 60 * 24;
|
||||
this.configuration = {
|
||||
expires,
|
||||
data: await res.json(),
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchJwk(): Promise<void> {
|
||||
const configuration = await this.getConfiguration();
|
||||
const res = await fetch(configuration.jwks_uri);
|
||||
const expires = res.headers.has("expires")
|
||||
? new Date(res.headers.get("expires")).getTime()
|
||||
: Date.now() + 1000 * 60 * 60 * 24;
|
||||
this.jwk = {
|
||||
expires,
|
||||
data: (await res.json())["keys"],
|
||||
};
|
||||
}
|
||||
|
||||
private deinit() {
|
||||
this.discoveryUri = undefined;
|
||||
this.configuration = undefined;
|
||||
this.jwk = undefined;
|
||||
}
|
||||
|
||||
private decodeIdToken(idToken: string): OidcIdToken {
|
||||
return this.jwtService.decode(idToken) as OidcIdToken;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OidcCache<T> {
|
||||
expires: number;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface OidcConfiguration {
|
||||
issuer: string;
|
||||
authorization_endpoint: string;
|
||||
token_endpoint: string;
|
||||
userinfo_endpoint?: string;
|
||||
jwks_uri: string;
|
||||
response_types_supported: string[];
|
||||
id_token_signing_alg_values_supported: string[];
|
||||
scopes_supported?: string[];
|
||||
claims_supported?: string[];
|
||||
}
|
||||
|
||||
export interface OidcJwk {
|
||||
e: string;
|
||||
alg: string;
|
||||
kid: string;
|
||||
use: string;
|
||||
kty: string;
|
||||
n: string;
|
||||
}
|
||||
|
||||
export type OidcConfigurationCache = OidcCache<OidcConfiguration>;
|
||||
|
||||
export type OidcJwkCache = OidcCache<OidcJwk[]>;
|
||||
|
||||
export interface OidcToken {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
id_token: string;
|
||||
}
|
||||
|
||||
export interface OidcIdToken {
|
||||
iss: string;
|
||||
sub: string;
|
||||
exp: number;
|
||||
iat: number;
|
||||
email: string;
|
||||
name: string;
|
||||
nickname: string;
|
||||
preferred_username: string;
|
||||
nonce: string;
|
||||
}
|
||||
112
backend/src/oauth/provider/github.provider.ts
Normal file
112
backend/src/oauth/provider/github.provider.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import fetch from "node-fetch";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
import { ErrorPageException } from "../exceptions/errorPage.exception";
|
||||
import { OAuthProvider, OAuthToken } from "./oauthProvider.interface";
|
||||
|
||||
@Injectable()
|
||||
export class GitHubProvider implements OAuthProvider<GitHubToken> {
|
||||
constructor(private config: ConfigService) {}
|
||||
|
||||
getAuthEndpoint(state: string): Promise<string> {
|
||||
return Promise.resolve(
|
||||
"https://github.com/login/oauth/authorize?" +
|
||||
new URLSearchParams({
|
||||
client_id: this.config.get("oauth.github-clientId"),
|
||||
redirect_uri:
|
||||
this.config.get("general.appUrl") + "/api/oauth/callback/github",
|
||||
state: state,
|
||||
scope: "user:email",
|
||||
}).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
async getToken(query: OAuthCallbackDto): Promise<OAuthToken<GitHubToken>> {
|
||||
const res = await fetch(
|
||||
"https://github.com/login/oauth/access_token?" +
|
||||
new URLSearchParams({
|
||||
client_id: this.config.get("oauth.github-clientId"),
|
||||
client_secret: this.config.get("oauth.github-clientSecret"),
|
||||
code: query.code,
|
||||
}).toString(),
|
||||
{
|
||||
method: "post",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const token: GitHubToken = await res.json();
|
||||
return {
|
||||
accessToken: token.access_token,
|
||||
tokenType: token.token_type,
|
||||
scope: token.scope,
|
||||
rawToken: token,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserInfo(token: OAuthToken<GitHubToken>): Promise<OAuthSignInDto> {
|
||||
if (!token.scope.includes("user:email")) {
|
||||
throw new ErrorPageException("no_email", undefined, ["provider_github"]);
|
||||
}
|
||||
const user = await this.getGitHubUser(token);
|
||||
const email = await this.getGitHubEmail(token);
|
||||
if (!email) {
|
||||
throw new ErrorPageException("no_email", undefined, ["provider_github"]);
|
||||
}
|
||||
|
||||
return {
|
||||
provider: "github",
|
||||
providerId: user.id.toString(),
|
||||
providerUsername: user.name ?? user.login,
|
||||
email,
|
||||
};
|
||||
}
|
||||
|
||||
private async getGitHubUser(
|
||||
token: OAuthToken<GitHubToken>,
|
||||
): Promise<GitHubUser> {
|
||||
const res = await fetch("https://api.github.com/user", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `${token.tokenType ?? "Bearer"} ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
return (await res.json()) as GitHubUser;
|
||||
}
|
||||
|
||||
private async getGitHubEmail(
|
||||
token: OAuthToken<GitHubToken>,
|
||||
): Promise<string | undefined> {
|
||||
const res = await fetch("https://api.github.com/user/public_emails", {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `${token.tokenType ?? "Bearer"} ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
const emails = (await res.json()) as GitHubEmail[];
|
||||
return emails.find((e) => e.primary && e.verified)?.email;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GitHubToken {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
export interface GitHubUser {
|
||||
login: string;
|
||||
id: number;
|
||||
name?: string;
|
||||
email?: string; // this filed seems only return null
|
||||
}
|
||||
|
||||
export interface GitHubEmail {
|
||||
email: string;
|
||||
primary: boolean;
|
||||
verified: boolean;
|
||||
visibility: string | null;
|
||||
}
|
||||
21
backend/src/oauth/provider/google.provider.ts
Normal file
21
backend/src/oauth/provider/google.provider.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { GenericOidcProvider } from "./genericOidc.provider";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { Cache } from "cache-manager";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleProvider extends GenericOidcProvider {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
jwtService: JwtService,
|
||||
@Inject(CACHE_MANAGER) cache: Cache,
|
||||
) {
|
||||
super("google", ["oauth.google-enabled"], config, jwtService, cache);
|
||||
}
|
||||
|
||||
protected getDiscoveryUri(): string {
|
||||
return "https://accounts.google.com/.well-known/openid-configuration";
|
||||
}
|
||||
}
|
||||
29
backend/src/oauth/provider/microsoft.provider.ts
Normal file
29
backend/src/oauth/provider/microsoft.provider.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { GenericOidcProvider } from "./genericOidc.provider";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { Cache } from "cache-manager";
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftProvider extends GenericOidcProvider {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
jwtService: JwtService,
|
||||
@Inject(CACHE_MANAGER) cache: Cache,
|
||||
) {
|
||||
super(
|
||||
"microsoft",
|
||||
["oauth.microsoft-enabled", "oauth.microsoft-tenant"],
|
||||
config,
|
||||
jwtService,
|
||||
cache,
|
||||
);
|
||||
}
|
||||
|
||||
protected getDiscoveryUri(): string {
|
||||
return `https://login.microsoftonline.com/${this.config.get(
|
||||
"oauth.microsoft-tenant",
|
||||
)}/v2.0/.well-known/openid-configuration`;
|
||||
}
|
||||
}
|
||||
24
backend/src/oauth/provider/oauthProvider.interface.ts
Normal file
24
backend/src/oauth/provider/oauthProvider.interface.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
|
||||
/**
|
||||
* @typeParam T - type of token
|
||||
* @typeParam C - type of callback query
|
||||
*/
|
||||
export interface OAuthProvider<T, C = OAuthCallbackDto> {
|
||||
getAuthEndpoint(state: string): Promise<string>;
|
||||
|
||||
getToken(query: C): Promise<OAuthToken<T>>;
|
||||
|
||||
getUserInfo(token: OAuthToken<T>, query: C): Promise<OAuthSignInDto>;
|
||||
}
|
||||
|
||||
export interface OAuthToken<T> {
|
||||
accessToken: string;
|
||||
expiresIn?: number;
|
||||
refreshToken?: string;
|
||||
tokenType?: string;
|
||||
scope?: string;
|
||||
idToken?: string;
|
||||
rawToken: T;
|
||||
}
|
||||
39
backend/src/oauth/provider/oidc.provider.ts
Normal file
39
backend/src/oauth/provider/oidc.provider.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { GenericOidcProvider, OidcToken } from "./genericOidc.provider";
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "../../config/config.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||
import { Cache } from "cache-manager";
|
||||
import { OAuthCallbackDto } from "../dto/oauthCallback.dto";
|
||||
import { OAuthSignInDto } from "../dto/oauthSignIn.dto";
|
||||
import { OAuthToken } from "./oauthProvider.interface";
|
||||
|
||||
@Injectable()
|
||||
export class OidcProvider extends GenericOidcProvider {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
jwtService: JwtService,
|
||||
@Inject(CACHE_MANAGER) protected cache: Cache,
|
||||
) {
|
||||
super(
|
||||
"oidc",
|
||||
["oauth.oidc-enabled", "oauth.oidc-discoveryUri"],
|
||||
config,
|
||||
jwtService,
|
||||
cache,
|
||||
);
|
||||
}
|
||||
|
||||
protected getDiscoveryUri(): string {
|
||||
return this.config.get("oauth.oidc-discoveryUri");
|
||||
}
|
||||
|
||||
getUserInfo(
|
||||
token: OAuthToken<OidcToken>,
|
||||
query: OAuthCallbackDto,
|
||||
_?: string,
|
||||
): Promise<OAuthSignInDto> {
|
||||
const claim = this.config.get("oauth.oidc-usernameClaim") || undefined;
|
||||
return super.getUserInfo(token, query, claim);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as moment from "moment";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { FileService } from "src/file/file.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { parseRelativeDateToAbsolute } from "src/utils/date.util";
|
||||
import { CreateReverseShareDTO } from "./dto/createReverseShare.dto";
|
||||
|
||||
@Injectable()
|
||||
@@ -24,6 +25,17 @@ export class ReverseShareService {
|
||||
)
|
||||
.toDate();
|
||||
|
||||
const parsedExpiration = parseRelativeDateToAbsolute(data.shareExpiration);
|
||||
if (
|
||||
this.config.get("share.maxExpiration") !== 0 &&
|
||||
parsedExpiration >
|
||||
moment().add(this.config.get("share.maxExpiration"), "hours").toDate()
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Expiration date exceeds maximum expiration date",
|
||||
);
|
||||
}
|
||||
|
||||
const globalMaxShareSize = this.config.get("share.maxSize");
|
||||
|
||||
if (globalMaxShareSize < data.maxShareSize)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
@@ -7,10 +6,17 @@ import {
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { JwtGuard } from "../../auth/guard/jwt.guard";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class ShareOwnerGuard implements CanActivate {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
export class ShareOwnerGuard extends JwtGuard {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
private prisma: PrismaService,
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
@@ -30,6 +36,8 @@ export class ShareOwnerGuard implements CanActivate {
|
||||
|
||||
if (!share.creatorId) return true;
|
||||
|
||||
if (!(await super.canActivate(context))) return false;
|
||||
|
||||
return share.creatorId == (request.user as User).id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,12 @@ export class ShareController {
|
||||
return new ShareDTO().from(await this.shareService.get(id));
|
||||
}
|
||||
|
||||
@Get(":id/from-owner")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async getFromOwner(@Param("id") id: string) {
|
||||
return new ShareDTO().from(await this.shareService.get(id));
|
||||
}
|
||||
|
||||
@Get(":id/metaData")
|
||||
@UseGuards(ShareSecurityGuard)
|
||||
async getMetaData(@Param("id") id: string) {
|
||||
@@ -62,12 +68,6 @@ export class ShareController {
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(JwtGuard, ShareOwnerGuard)
|
||||
async remove(@Param("id") id: string) {
|
||||
await this.shareService.remove(id);
|
||||
}
|
||||
|
||||
@Post(":id/complete")
|
||||
@HttpCode(202)
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
@@ -78,6 +78,18 @@ export class ShareController {
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(":id/complete")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async revertComplete(@Param("id") id: string) {
|
||||
return new ShareDTO().from(await this.shareService.revertComplete(id));
|
||||
}
|
||||
|
||||
@Delete(":id")
|
||||
@UseGuards(ShareOwnerGuard)
|
||||
async remove(@Param("id") id: string) {
|
||||
await this.shareService.remove(id);
|
||||
}
|
||||
|
||||
@Throttle(10, 60)
|
||||
@Get("isShareIdAvailable/:id")
|
||||
async isShareIdAvailable(@Param("id") id: string) {
|
||||
|
||||
@@ -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 { parseRelativeDateToAbsolute } from "src/utils/date.util";
|
||||
import { SHARE_DIRECTORY } from "../constants";
|
||||
import { CreateShareDTO } from "./dto/createShare.dto";
|
||||
|
||||
@@ -51,19 +52,24 @@ export class ShareService {
|
||||
if (reverseShare) {
|
||||
expirationDate = reverseShare.shareExpiration;
|
||||
} else {
|
||||
// We have to add an exception for "never" (since moment won't like that)
|
||||
if (share.expiration !== "never") {
|
||||
expirationDate = moment()
|
||||
.add(
|
||||
share.expiration.split("-")[0],
|
||||
share.expiration.split(
|
||||
"-",
|
||||
)[1] as moment.unitOfTime.DurationConstructor,
|
||||
)
|
||||
.toDate();
|
||||
} else {
|
||||
expirationDate = moment(0).toDate();
|
||||
const parsedExpiration = parseRelativeDateToAbsolute(share.expiration);
|
||||
|
||||
const expiresNever = moment(0).toDate() == parsedExpiration;
|
||||
|
||||
if (
|
||||
this.config.get("share.maxExpiration") !== 0 &&
|
||||
(expiresNever ||
|
||||
parsedExpiration >
|
||||
moment()
|
||||
.add(this.config.get("share.maxExpiration"), "hours")
|
||||
.toDate())
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"Expiration date exceeds maximum expiration date",
|
||||
);
|
||||
}
|
||||
|
||||
expirationDate = parsedExpiration;
|
||||
}
|
||||
|
||||
fs.mkdirSync(`${SHARE_DIRECTORY}/${share.id}`, {
|
||||
@@ -181,6 +187,13 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
async revertComplete(id: string) {
|
||||
return this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: false, isZipReady: false },
|
||||
});
|
||||
}
|
||||
|
||||
async getSharesByUser(userId: string) {
|
||||
const shares = await this.prisma.share.findMany({
|
||||
where: {
|
||||
|
||||
@@ -16,6 +16,9 @@ export class UserDTO {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Expose()
|
||||
hasPassword: boolean;
|
||||
|
||||
@MinLength(8)
|
||||
password: string;
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ export class UserController {
|
||||
@Get("me")
|
||||
@UseGuards(JwtGuard)
|
||||
async getCurrentUser(@GetUser() user: User) {
|
||||
return new UserDTO().from(user);
|
||||
const userDTO = new UserDTO().from(user);
|
||||
userDTO.hasPassword = !!user.password;
|
||||
return userDTO;
|
||||
}
|
||||
|
||||
@Patch("me")
|
||||
|
||||
12
backend/src/utils/date.util.ts
Normal file
12
backend/src/utils/date.util.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as moment from "moment";
|
||||
|
||||
export function parseRelativeDateToAbsolute(relativeDate: string) {
|
||||
if (relativeDate == "never") return moment(0).toDate();
|
||||
|
||||
return moment()
|
||||
.add(
|
||||
relativeDate.split("-")[0],
|
||||
relativeDate.split("-")[1] as moment.unitOfTime.DurationConstructor,
|
||||
)
|
||||
.toDate();
|
||||
}
|
||||
@@ -6,7 +6,10 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"target": "es2021",
|
||||
"lib": [
|
||||
"ES2021"
|
||||
],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
|
||||
_Leer esto en otro idioma: [Inglés](/README.md), [Español](/docs/README.es.md), [Chino Simplificado](/docs/README.zh-cn.md)_
|
||||
_Leer esto en otro idioma: [Inglés](/README.md), [Español](/docs/README.es.md), [Chino Simplificado](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
@@ -20,7 +20,8 @@ Pingvin Share es una plataforma de intercambio de archivos autoalojada y una alt
|
||||
## 🐧 Conoce Pingvin Share
|
||||
|
||||
- [Demo](https://pingvin-share.dev.eliasschneider.com)
|
||||
- [Reseña por DB Tech](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
- [Reseña realizada por No Solo Hacking (español)](https://www.youtube.com/watch?v=ocd4EpLTYkU)
|
||||
- [Reseña por DB Tech (inglés)](https://www.youtube.com/watch?v=rWwNeZCOPJA)
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/58886915/225038319-b2ef742c-3a74-4eb6-9689-4207a36842a4.png" width="700"/>
|
||||
|
||||
|
||||
158
docs/README.ja-jp.md
Normal file
158
docs/README.ja-jp.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# <div align="center"><img src="https://user-images.githubusercontent.com/58886915/166198400-c2134044-1198-4647-a8b6-da9c4a204c68.svg" width="40"/> </br>Pingvin Share</div>
|
||||
|
||||
---
|
||||
|
||||
_READMEを別の言語で読む: [Spanish](/docs/README.es.md), [English](/README.md), [Simplified Chinese](/docs/README.zh-cn.md), [日本語](/docs/README.ja-jp.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は、早期段階であり、バグが含まれている場合があります。
|
||||
|
||||
### Dockerでインストール (おすすめ)
|
||||
|
||||
1. `docker-compose.yml`ファイルをダウンロード
|
||||
2. `docker-compose up -d`を実行
|
||||
|
||||
Webサイトは、`http://localhost:3000`でリッスンされます。これでPingvin Shareをお使い頂けます🐧!
|
||||
|
||||
### スタンドアローンインストール
|
||||
|
||||
必要なツール:
|
||||
|
||||
- [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`)
|
||||
|
||||
# バックエンドを開始
|
||||
cd backend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-backend" npm -- run prod
|
||||
|
||||
#フロントエンドを開始
|
||||
cd ../frontend
|
||||
npm install
|
||||
npm run build
|
||||
pm2 start --name="pingvin-share-frontend" npm -- run start
|
||||
```
|
||||
|
||||
Webサイトは、`http://localhost:3000`でリッスンされます。これでPingvin Shareをお使い頂けます🐧!
|
||||
|
||||
### 連携機能
|
||||
|
||||
#### ClamAV (Dockerのみ)
|
||||
|
||||
ClamAVは、共有されたファイルをスキャンし、感染したファイルを見つけた場合に削除するために使用されます。
|
||||
|
||||
1. ClamAVコンテナをDocker Composeの定義ファイル(`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)をご確認ください。
|
||||
|
||||
### 追加情報
|
||||
|
||||
- [Synology NASへのインストール方法](https://mariushosting.com/how-to-install-pingvin-share-on-your-synology-nas/)
|
||||
|
||||
### 新しいバージョンへのアップグレード
|
||||
|
||||
Pingvin Shareは早期段階のため、アップグレード前に必ずリリースノートを確認して、アップグレードしても問題ないかどうかご確認ください。
|
||||
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### スタンドアローン
|
||||
|
||||
1. アプリを停止する
|
||||
```bash
|
||||
pm2 stop pingvin-share-backend pingvin-share-frontend
|
||||
```
|
||||
2. `git clone`のステップを除いて、[インストールガイド](#stand-alone-installation)をくり返してください。
|
||||
|
||||
```bash
|
||||
cd pingvin-share
|
||||
|
||||
# 最新バージョンをチェックアウト
|
||||
git fetch --tags && git checkout $(git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
# バックエンドを開始
|
||||
cd backend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-backend
|
||||
|
||||
#フロントエンドを開始
|
||||
cd ../frontend
|
||||
npm run build
|
||||
pm2 restart pingvin-share-frontend
|
||||
```
|
||||
|
||||
### 設定
|
||||
|
||||
管理者のダッシュボード内の「設定」ページから、Pingvin Shareをカスタマイズできます。
|
||||
|
||||
#### 環境変数
|
||||
|
||||
インストール時の特定の設定で、環境変数を使用できます。次の環境変数が使用可能です:
|
||||
|
||||
##### バックエンド
|
||||
|
||||
| 変数名 | デフォルト値 | 説明 |
|
||||
| ---------------- | -------------------------------------------------- | -------------------------------------- |
|
||||
| `PORT` | `8080` | バックエンドがリッスンするポート番号 |
|
||||
| `DATABASE_URL` | `file:../data/pingvin-share.db?connection_limit=1` | SQLiteのURL |
|
||||
| `DATA_DIRECTORY` | `./data` | データを保管するディレクトリ |
|
||||
| `CLAMAV_HOST` | `127.0.0.1` | ClamAVサーバーのIPアドレス |
|
||||
| `CLAMAV_PORT` | `3310` | ClamAVサーバーのポート番号 |
|
||||
|
||||
##### フロントエンド
|
||||
|
||||
| 変数名 | デフォルト値 | 説明 |
|
||||
| --------- | ----------------------- | ---------------------------------------- |
|
||||
| `PORT` | `3000` | フロントエンドがリッスンするポート番号 |
|
||||
| `API_URL` | `http://localhost:8080` | フロントエンドからアクセスするバックエンドへのURL |
|
||||
|
||||
## 🖤 コントリビュート
|
||||
|
||||
### 翻訳
|
||||
|
||||
Pingvin Shareをあなたが使用している言語に翻訳するお手伝いを募集しています。
|
||||
[Crowdin](https://crowdin.com/project/pingvin-share)上で、簡単にPingvin Shareの翻訳作業への参加が可能です。
|
||||
|
||||
あなたの言語がありませんか? 気軽に[リクエスト](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)してください。
|
||||
|
||||
翻訳中に問題がありましたか? [ローカライズに関するディスカッション](https://github.com/stonith404/pingvin-share/discussions/198)に是非参加してください。
|
||||
|
||||
### プロジェクト
|
||||
|
||||
Pingvin Shareへのコントリビュートをいつでもお待ちしています! [コントリビューションガイド](/CONTRIBUTING.md)を確認して、是非参加してください。
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
---
|
||||
|
||||
_选择合适的语言阅读: [西班牙语](/docs/README.es.md), [英语](/README.md), [简体中文](/docs/README.zh-cn.md)_
|
||||
_选择合适的语言阅读: [西班牙语](/docs/README.es.md), [英语](/README.md), [简体中文](/docs/README.zh-cn.md), [日本语](/docs/README.ja-jp.md)_
|
||||
|
||||
---
|
||||
|
||||
|
||||
157
docs/oauth2-guide.md
Normal file
157
docs/oauth2-guide.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# OAuth 2 Login Guide
|
||||
|
||||
## Config Built-in OAuth 2 Providers
|
||||
|
||||
- [GitHub](#github)
|
||||
- [Google](#google)
|
||||
- [Microsoft](#microsoft)
|
||||
- [Discord](#discord)
|
||||
- [OpenID Connect](#openid-connect)
|
||||
|
||||
### GitHub
|
||||
|
||||
Please follow the [official guide](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) to create an OAuth app.
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/github`
|
||||
|
||||
### Google
|
||||
|
||||
Please follow the [official guide](https://developers.google.com/identity/protocols/oauth2/web-server#prerequisites) to create an OAuth 2.0 App.
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/google`
|
||||
|
||||
### Microsoft
|
||||
|
||||
Please follow the [official guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) to register an application.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Microsoft Tenant** you set in the admin panel must match the **supported account types** you set in the Microsoft Entra admin center, otherwise the OAuth login will not work. Refer to the [official documentation](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri) for more details.
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/microsoft`
|
||||
|
||||
### Discord
|
||||
|
||||
Create an application on [Discord Developer Portal](https://discord.com/developers/applications).
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/discord`
|
||||
|
||||
### OpenID Connect
|
||||
|
||||
Generic OpenID Connect provider is also supported, we have tested it on Keycloak, Authentik and Casdoor.
|
||||
|
||||
Redirect URL: `https://<your-domain>/api/oauth/callback/oidc`
|
||||
|
||||
## Custom your OAuth 2 Provider
|
||||
|
||||
If our built-in providers don't meet your needs, you can create your own OAuth 2 provider.
|
||||
|
||||
### 1. Create config
|
||||
|
||||
Add your config (client id, client secret, etc.) in [`config.seed.ts`](../backend/prisma/seed/config.seed.ts):
|
||||
|
||||
```ts
|
||||
const configVariables: ConfigVariables = {
|
||||
// ...
|
||||
oauth: {
|
||||
// ...
|
||||
"YOUR_PROVIDER_NAME-enabled": {
|
||||
type: "boolean",
|
||||
defaultValue: "false",
|
||||
},
|
||||
"YOUR_PROVIDER_NAME-clientId": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
},
|
||||
"YOUR_PROVIDER_NAME-clientSecret": {
|
||||
type: "string",
|
||||
defaultValue: "",
|
||||
obscured: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Create provider class
|
||||
|
||||
#### Generic OpenID Connect
|
||||
|
||||
If your provider supports OpenID connect, it's extremely easy to extend [`GenericOidcProvider`](../backend/src/oauth/provider/genericOidc.provider.ts) to add a new OpenID Connect provider.
|
||||
|
||||
The [Google provider](../backend/src/oauth/provider/google.provider.ts) and [Microsoft provider](../backend/src/oauth/provider/microsoft.provider.ts) are good examples.
|
||||
|
||||
Here are some discovery URIs for popular providers:
|
||||
|
||||
- Microsoft: `https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration`
|
||||
- Google: `https://accounts.google.com/.well-known/openid-configuration`
|
||||
- Apple: `https://appleid.apple.com/.well-known/openid-configuration`
|
||||
- Gitlab: `https://gitlab.com/.well-known/openid-configuration`
|
||||
- Huawei: `https://oauth-login.cloud.huawei.com/.well-known/openid-configuration`
|
||||
- Paypal: `https://www.paypal.com/.well-known/openid-configuration`
|
||||
- Yahoo: `https://api.login.yahoo.com/.well-known/openid-configuration`
|
||||
|
||||
#### OAuth 2
|
||||
|
||||
If your provider only supports OAuth 2, you can implement [`OAuthProvider`](../backend/src/oauth/provider/oauthProvider.interface.ts) interface to add a new OAuth 2 provider.
|
||||
|
||||
The [GitHub provider](../backend/src/oauth/provider/github.provider.ts) and [Discord provider](../backend/src/oauth/provider/discord.provider.ts) are good examples.
|
||||
|
||||
### 3. Register provider
|
||||
|
||||
Register your provider in [`OAuthModule`](../backend/src/oauth/oauth.module.ts) and [`OAuthSignInDto`](../backend/src/oauth/dto/oauthSignIn.dto.ts):
|
||||
|
||||
```ts
|
||||
@Module({
|
||||
providers: [
|
||||
GitHubProvider,
|
||||
// your provider
|
||||
{
|
||||
provide: "OAUTH_PROVIDERS",
|
||||
useFactory(github: GitHubProvider, /* your provider */): Record<string, OAuthProvider<unknown>> {
|
||||
return {
|
||||
github,
|
||||
/* your provider */
|
||||
};
|
||||
},
|
||||
inject: [GitHubProvider, /* your provider */],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class OAuthModule {
|
||||
}
|
||||
```
|
||||
|
||||
```ts
|
||||
export interface OAuthSignInDto {
|
||||
provider: 'github' | 'google' | 'microsoft' | 'discord' | 'oidc' /* your provider*/;
|
||||
providerId: string;
|
||||
providerUsername: string;
|
||||
email: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add frontend icon
|
||||
|
||||
Add an icon in [`oauth.util.tsx`](../frontend/src/utils/oauth.util.tsx).
|
||||
|
||||
```tsx
|
||||
const getOAuthIcon = (provider: string) => {
|
||||
return {
|
||||
'github': <SiGithub />,
|
||||
/* your provider */
|
||||
}[provider];
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add i18n text
|
||||
|
||||
Add keys below to your i18n text in [locale file](../frontend/src/i18n/translations/en-US.ts).
|
||||
|
||||
- `signIn.oauth.YOUR_PROVIDER_NAME`
|
||||
- `account.card.oauth.YOUR_PROVIDER_NAME`
|
||||
- `admin.config.oauth.YOUR_PROVIDER_NAME-enabled`
|
||||
- `admin.config.oauth.YOUR_PROVIDER_NAME-client-id`
|
||||
- `admin.config.oauth.YOUR_PROVIDER_NAME-client-secret`
|
||||
- `error.param.provider_YOUR_PROVIDER_NAME`
|
||||
- Other config keys you defined in step 1
|
||||
|
||||
Congratulations! 🎉 You have successfully added a new OAuth 2 provider! Pull requests are welcome if you want to share your provider with others.
|
||||
642
frontend/package-lock.json
generated
642
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.18.2",
|
||||
"version": "0.21.4",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
|
||||
@@ -36,28 +36,28 @@ const showShareInformationsModal = (
|
||||
|
||||
children: (
|
||||
<Stack align="stretch" spacing="md">
|
||||
<Text size="sm" color="lightgray">
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.id" />:{" "}
|
||||
</b>
|
||||
{share.id}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.description" />:{" "}
|
||||
</b>
|
||||
{share.description || "No description"}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.createdAt" />:{" "}
|
||||
</b>
|
||||
{formattedCreatedAt}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" color="lightgray">
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.expiresAt" />:{" "}
|
||||
</b>
|
||||
@@ -66,7 +66,7 @@ const showShareInformationsModal = (
|
||||
<Divider />
|
||||
<CopyTextField link={link} />
|
||||
<Divider />
|
||||
<Text size="sm" color="lightgray">
|
||||
<Text size="sm">
|
||||
<b>
|
||||
<FormattedMessage id="account.shares.table.size" />:{" "}
|
||||
</b>
|
||||
@@ -76,7 +76,7 @@ const showShareInformationsModal = (
|
||||
|
||||
<Flex align="center" justify="center">
|
||||
{shareSize / maxShareSize < 0.1 && (
|
||||
<Text size="xs" color="lightgray" style={{ marginRight: "4px" }}>
|
||||
<Text size="xs" style={{ marginRight: "4px" }}>
|
||||
{formattedShareSize}
|
||||
</Text>
|
||||
)}
|
||||
@@ -87,7 +87,7 @@ const showShareInformationsModal = (
|
||||
size="xl"
|
||||
radius="xl"
|
||||
/>
|
||||
<Text size="xs" color="lightgray" style={{ marginLeft: "4px" }}>
|
||||
<Text size="xs" style={{ marginLeft: "4px" }}>
|
||||
{formattedMaxShareSize}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbAt, TbMail, TbShare, TbSquare } from "react-icons/tb";
|
||||
import { TbAt, TbMail, TbShare, TbSocial, TbSquare } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const categories = [
|
||||
@@ -19,6 +19,7 @@ const categories = [
|
||||
{ name: "Email", icon: <TbMail /> },
|
||||
{ name: "Share", icon: <TbShare /> },
|
||||
{ name: "SMTP", icon: <TbAt /> },
|
||||
{ name: "OAuth", icon: <TbSocial /> },
|
||||
];
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
|
||||
@@ -2,9 +2,11 @@ import {
|
||||
Anchor,
|
||||
Button,
|
||||
Container,
|
||||
createStyles,
|
||||
Group,
|
||||
Paper,
|
||||
PasswordInput,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
@@ -18,19 +20,47 @@ 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 useTranslate from "../../hooks/useTranslate.hook";
|
||||
import authService from "../../services/auth.service";
|
||||
import { getOAuthIcon, getOAuthUrl } from "../../utils/oauth.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
or: {
|
||||
"&:before": {
|
||||
content: "''",
|
||||
flex: 1,
|
||||
display: "block",
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[3]
|
||||
: theme.colors.gray[4],
|
||||
},
|
||||
"&:after": {
|
||||
content: "''",
|
||||
flex: 1,
|
||||
display: "block",
|
||||
borderTopWidth: 1,
|
||||
borderTopStyle: "solid",
|
||||
borderColor:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.dark[3]
|
||||
: theme.colors.gray[4],
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
const config = useConfig();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
const { refreshUser } = useUser();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const [showTotp, setShowTotp] = React.useState(false);
|
||||
const [loginToken, setLoginToken] = React.useState("");
|
||||
const [oauth, setOAuth] = React.useState<string[]>([]);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
emailOrUsername: yup.string().required(t("common.error.field-required")),
|
||||
@@ -44,7 +74,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
initialValues: {
|
||||
emailOrUsername: "",
|
||||
password: "",
|
||||
totp: "",
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -55,7 +84,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
.then(async (response) => {
|
||||
if (response.data["loginToken"]) {
|
||||
// Prompt the user to enter their totp code
|
||||
setShowTotp(true);
|
||||
showNotification({
|
||||
icon: <TbInfoCircle />,
|
||||
color: "blue",
|
||||
@@ -63,7 +91,11 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
title: t("signIn.notify.totp-required.title"),
|
||||
message: t("signIn.notify.totp-required.description"),
|
||||
});
|
||||
setLoginToken(response.data["loginToken"]);
|
||||
router.push(
|
||||
`/auth/totp/${
|
||||
response.data["loginToken"]
|
||||
}?redirect=${encodeURIComponent(redirectPath)}`,
|
||||
);
|
||||
} else {
|
||||
await refreshUser();
|
||||
router.replace(redirectPath);
|
||||
@@ -72,25 +104,15 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
.catch(toast.axiosError);
|
||||
};
|
||||
|
||||
const signInTotp = (email: string, password: string, totp: string) => {
|
||||
authService
|
||||
.signInTotp(email, password, totp, loginToken)
|
||||
.then(async () => {
|
||||
await refreshUser();
|
||||
router.replace(redirectPath);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error?.response?.data?.error == "share_password_required") {
|
||||
toast.axiosError(error);
|
||||
// Refresh the page to start over
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
toast.axiosError(error);
|
||||
form.setValues({ totp: "" });
|
||||
});
|
||||
const getAvailableOAuth = async () => {
|
||||
const oauth = await authService.getAvailableOAuth();
|
||||
setOAuth(oauth.data);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
getAvailableOAuth().catch(toast.axiosError);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title order={2} align="center" weight={900}>
|
||||
@@ -107,9 +129,7 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
if (showTotp)
|
||||
signInTotp(values.emailOrUsername, values.password, values.totp);
|
||||
else signIn(values.emailOrUsername, values.password);
|
||||
signIn(values.emailOrUsername, values.password);
|
||||
})}
|
||||
>
|
||||
<TextInput
|
||||
@@ -123,15 +143,6 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
mt="md"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
{showTotp && (
|
||||
<TextInput
|
||||
variant="filled"
|
||||
label={t("account.modal.totp.code")}
|
||||
placeholder="******"
|
||||
mt="md"
|
||||
{...form.getInputProps("totp")}
|
||||
/>
|
||||
)}
|
||||
{config.get("smtp.enabled") && (
|
||||
<Group position="right" mt="xs">
|
||||
<Anchor component={Link} href="/auth/resetPassword" size="xs">
|
||||
@@ -143,6 +154,27 @@ const SignInForm = ({ redirectPath }: { redirectPath: string }) => {
|
||||
<FormattedMessage id="signin.button.submit" />
|
||||
</Button>
|
||||
</form>
|
||||
{oauth.length > 0 && (
|
||||
<Stack mt="xl">
|
||||
<Group align="center" className={classes.or}>
|
||||
<Text>{t("signIn.oauth.or")}</Text>
|
||||
</Group>
|
||||
<Group position="center">
|
||||
{oauth.map((provider) => (
|
||||
<Button
|
||||
key={provider}
|
||||
component="a"
|
||||
target="_blank"
|
||||
title={t(`signIn.oauth.${provider}`)}
|
||||
href={getOAuthUrl(config.get("general.appUrl"), provider)}
|
||||
variant="light"
|
||||
>
|
||||
{getOAuthIcon(provider)}
|
||||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
|
||||
84
frontend/src/components/auth/TotpForm.tsx
Normal file
84
frontend/src/components/auth/TotpForm.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Group,
|
||||
Paper,
|
||||
PinInput,
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useState } from "react";
|
||||
import authService from "../../services/auth.service";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
import useUser from "../../hooks/user.hook";
|
||||
|
||||
function TotpForm({ redirectPath }: { redirectPath: string }) {
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
const { refreshUser } = useUser();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
code: yup
|
||||
.string()
|
||||
.min(6, t("common.error.too-short", { length: 6 }))
|
||||
.required(t("common.error.field-required")),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
code: "",
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await authService.signInTotp(
|
||||
form.values.code,
|
||||
router.query.loginToken as string,
|
||||
);
|
||||
await refreshUser();
|
||||
await router.replace(redirectPath);
|
||||
} catch (e) {
|
||||
toast.axiosError(e);
|
||||
form.setFieldError("code", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size={420} my={40}>
|
||||
<Title order={2} align="center" weight={900}>
|
||||
<FormattedMessage id="totp.title" />
|
||||
</Title>
|
||||
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Group position="center">
|
||||
<PinInput
|
||||
length={6}
|
||||
oneTimeCode
|
||||
aria-label="One time code"
|
||||
autoFocus={true}
|
||||
onComplete={onSubmit}
|
||||
{...form.getInputProps("code")}
|
||||
/>
|
||||
<Button mt="md" type="submit" loading={loading}>
|
||||
{t("totp.button.signIn")}
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default TotpForm;
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { useForm } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import moment from "moment";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
@@ -25,6 +26,7 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
|
||||
const showCreateReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
showSendEmailNotificationOption: boolean,
|
||||
maxExpirationInHours: number,
|
||||
getReverseShares: () => void,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
@@ -34,6 +36,7 @@ const showCreateReverseShareModal = (
|
||||
<Body
|
||||
showSendEmailNotificationOption={showSendEmailNotificationOption}
|
||||
getReverseShares={getReverseShares}
|
||||
maxExpirationInHours={maxExpirationInHours}
|
||||
/>
|
||||
),
|
||||
});
|
||||
@@ -42,9 +45,11 @@ const showCreateReverseShareModal = (
|
||||
const Body = ({
|
||||
getReverseShares,
|
||||
showSendEmailNotificationOption,
|
||||
maxExpirationInHours,
|
||||
}: {
|
||||
getReverseShares: () => void;
|
||||
showSendEmailNotificationOption: boolean;
|
||||
maxExpirationInHours: number;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
@@ -58,27 +63,48 @@ const Body = ({
|
||||
expiration_unit: "-days",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
const expirationDate = moment().add(
|
||||
form.values.expiration_num,
|
||||
form.values.expiration_unit.replace(
|
||||
"-",
|
||||
"",
|
||||
) as moment.unitOfTime.DurationConstructor,
|
||||
);
|
||||
if (
|
||||
maxExpirationInHours != 0 &&
|
||||
expirationDate.isAfter(moment().add(maxExpirationInHours, "hours"))
|
||||
) {
|
||||
form.setFieldError(
|
||||
"expiration_num",
|
||||
t("upload.modal.expires.error.too-long", {
|
||||
max: moment.duration(maxExpirationInHours, "hours").humanize(),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
shareService
|
||||
.createReverseShare(
|
||||
values.expiration_num + values.expiration_unit,
|
||||
values.maxShareSize,
|
||||
values.maxUseCount,
|
||||
values.sendEmailNotification,
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
showCompletedReverseShareModal(modals, link, getReverseShares);
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
});
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
shareService
|
||||
.createReverseShare(
|
||||
values.expiration_num + values.expiration_unit,
|
||||
values.maxShareSize,
|
||||
values.maxUseCount,
|
||||
values.sendEmailNotification,
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
showCompletedReverseShareModal(modals, link, getReverseShares);
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
})}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack align="stretch">
|
||||
<div>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ActionIcon, TextInput } from "@mantine/core";
|
||||
import { ActionIcon, TextInput, Tooltip } from "@mantine/core";
|
||||
import { useClipboard } from "@mantine/hooks";
|
||||
import { useRef, useState } from "react";
|
||||
import { TbCheck, TbCopy } from "react-icons/tb";
|
||||
import { IoOpenOutline } from "react-icons/io5";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
@@ -37,12 +38,27 @@ function CopyTextField(props: { link: string }) {
|
||||
setTextClicked(true);
|
||||
}
|
||||
}}
|
||||
rightSectionWidth={62}
|
||||
rightSection={
|
||||
window.isSecureContext && (
|
||||
<ActionIcon onClick={copyLink}>
|
||||
{checkState ? <TbCheck /> : <TbCopy />}
|
||||
</ActionIcon>
|
||||
)
|
||||
<>
|
||||
<Tooltip
|
||||
label={t("common.text.navigate-to-link")}
|
||||
position="top"
|
||||
offset={-2}
|
||||
openDelay={200}
|
||||
>
|
||||
<a href={props.link}>
|
||||
<ActionIcon>
|
||||
<IoOpenOutline />
|
||||
</ActionIcon>
|
||||
</a>
|
||||
</Tooltip>
|
||||
{window.isSecureContext && (
|
||||
<ActionIcon onClick={copyLink}>
|
||||
{checkState ? <TbCheck /> : <TbCopy />}
|
||||
</ActionIcon>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -33,10 +33,12 @@ const useStyles = createStyles((theme) => ({
|
||||
}));
|
||||
|
||||
const Dropzone = ({
|
||||
title,
|
||||
isUploading,
|
||||
maxShareSize,
|
||||
showCreateUploadModalCallback,
|
||||
}: {
|
||||
title?: string;
|
||||
isUploading: boolean;
|
||||
maxShareSize: number;
|
||||
showCreateUploadModalCallback: (files: FileUpload[]) => void;
|
||||
@@ -60,7 +62,7 @@ const Dropzone = ({
|
||||
toast.error(
|
||||
t("upload.dropzone.notify.file-too-big", {
|
||||
maxSize: byteToHumanSizeString(maxShareSize),
|
||||
})
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
files = files.map((newFile) => {
|
||||
@@ -78,7 +80,7 @@ const Dropzone = ({
|
||||
<TbCloudUpload size={50} />
|
||||
</Group>
|
||||
<Text align="center" weight={700} size="lg" mt="xl">
|
||||
<FormattedMessage id="upload.dropzone.title" />
|
||||
{title || <FormattedMessage id="upload.dropzone.title" />}
|
||||
</Text>
|
||||
<Text align="center" size="sm" mt="xs" color="dimmed">
|
||||
<FormattedMessage
|
||||
|
||||
238
frontend/src/components/upload/EditableUpload.tsx
Normal file
238
frontend/src/components/upload/EditableUpload.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { Button, Group } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { cleanNotifications } from "@mantine/notifications";
|
||||
import { AxiosError } from "axios";
|
||||
import pLimit from "p-limit";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Dropzone from "../../components/upload/Dropzone";
|
||||
import FileList from "../../components/upload/FileList";
|
||||
import showCompletedUploadModal from "../../components/upload/modals/showCompletedUploadModal";
|
||||
import useConfig from "../../hooks/config.hook";
|
||||
import useTranslate from "../../hooks/useTranslate.hook";
|
||||
import shareService from "../../services/share.service";
|
||||
import { FileListItem, FileMetaData, FileUpload } from "../../types/File.type";
|
||||
import toast from "../../utils/toast.util";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const promiseLimit = pLimit(3);
|
||||
const chunkSize = 10 * 1024 * 1024; // 10MB
|
||||
let errorToastShown = false;
|
||||
|
||||
const EditableUpload = ({
|
||||
maxShareSize,
|
||||
shareId,
|
||||
files: savedFiles = [],
|
||||
}: {
|
||||
maxShareSize?: number;
|
||||
isReverseShare?: boolean;
|
||||
shareId: string;
|
||||
files?: FileMetaData[];
|
||||
}) => {
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
const config = useConfig();
|
||||
|
||||
const [existingFiles, setExistingFiles] =
|
||||
useState<Array<FileMetaData & { deleted?: boolean }>>(savedFiles);
|
||||
const [uploadingFiles, setUploadingFiles] = useState<FileUpload[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const existingAndUploadedFiles: FileListItem[] = useMemo(
|
||||
() => [...uploadingFiles, ...existingFiles],
|
||||
[existingFiles, uploadingFiles],
|
||||
);
|
||||
const dirty = useMemo(() => {
|
||||
return (
|
||||
existingFiles.some((file) => !!file.deleted) || !!uploadingFiles.length
|
||||
);
|
||||
}, [existingFiles, uploadingFiles]);
|
||||
|
||||
const setFiles = (files: FileListItem[]) => {
|
||||
const _uploadFiles = files.filter(
|
||||
(file) => "uploadingProgress" in file,
|
||||
) as FileUpload[];
|
||||
const _existingFiles = files.filter(
|
||||
(file) => !("uploadingProgress" in file),
|
||||
) as FileMetaData[];
|
||||
|
||||
setUploadingFiles(_uploadFiles);
|
||||
setExistingFiles(_existingFiles);
|
||||
};
|
||||
|
||||
maxShareSize ??= parseInt(config.get("share.maxSize"));
|
||||
|
||||
const uploadFiles = async (files: FileUpload[]) => {
|
||||
const fileUploadPromises = files.map(async (file, fileIndex) =>
|
||||
// Limit the number of concurrent uploads to 3
|
||||
promiseLimit(async () => {
|
||||
let fileId: string;
|
||||
|
||||
const setFileProgress = (progress: number) => {
|
||||
setUploadingFiles((files) =>
|
||||
files.map((file, callbackIndex) => {
|
||||
if (fileIndex == callbackIndex) {
|
||||
file.uploadingProgress = progress;
|
||||
}
|
||||
return file;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
setFileProgress(1);
|
||||
|
||||
let chunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
// If the file is 0 bytes, we still need to upload 1 chunk
|
||||
if (chunks == 0) chunks++;
|
||||
|
||||
for (let chunkIndex = 0; chunkIndex < chunks; chunkIndex++) {
|
||||
const from = chunkIndex * chunkSize;
|
||||
const to = from + chunkSize;
|
||||
const blob = file.slice(from, to);
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) =>
|
||||
await shareService
|
||||
.uploadFile(
|
||||
shareId,
|
||||
event,
|
||||
{
|
||||
id: fileId,
|
||||
name: file.name,
|
||||
},
|
||||
chunkIndex,
|
||||
chunks,
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
resolve(response);
|
||||
})
|
||||
.catch(reject);
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
|
||||
setFileProgress(((chunkIndex + 1) / chunks) * 100);
|
||||
} catch (e) {
|
||||
if (
|
||||
e instanceof AxiosError &&
|
||||
e.response?.data.error == "unexpected_chunk_index"
|
||||
) {
|
||||
// Retry with the expected chunk index
|
||||
chunkIndex = e.response!.data!.expectedChunkIndex - 1;
|
||||
continue;
|
||||
} else {
|
||||
setFileProgress(-1);
|
||||
// Retry after 5 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
chunkIndex = -1;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await Promise.all(fileUploadPromises);
|
||||
};
|
||||
|
||||
const removeFiles = async () => {
|
||||
const removedFiles = existingFiles.filter((file) => !!file.deleted);
|
||||
|
||||
if (removedFiles.length > 0) {
|
||||
await Promise.all(
|
||||
removedFiles.map(async (file) => {
|
||||
await shareService.removeFile(shareId, file.id);
|
||||
}),
|
||||
);
|
||||
|
||||
setExistingFiles(existingFiles.filter((file) => !file.deleted));
|
||||
}
|
||||
};
|
||||
|
||||
const revertComplete = async () => {
|
||||
await shareService.revertComplete(shareId).then();
|
||||
};
|
||||
|
||||
const completeShare = async () => {
|
||||
return await shareService.completeShare(shareId);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setIsUploading(true);
|
||||
|
||||
try {
|
||||
await revertComplete();
|
||||
await uploadFiles(uploadingFiles);
|
||||
|
||||
const hasFailed = uploadingFiles.some(
|
||||
(file) => file.uploadingProgress == -1,
|
||||
);
|
||||
|
||||
if (!hasFailed) {
|
||||
await removeFiles();
|
||||
}
|
||||
|
||||
await completeShare();
|
||||
|
||||
if (!hasFailed) {
|
||||
toast.success(t("share.edit.notify.save-success"));
|
||||
router.back();
|
||||
}
|
||||
} catch {
|
||||
toast.error(t("share.edit.notify.generic-error"));
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const appendFiles = (appendingFiles: FileUpload[]) => {
|
||||
setUploadingFiles([...appendingFiles, ...uploadingFiles]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there are any files that failed to upload
|
||||
const fileErrorCount = uploadingFiles.filter(
|
||||
(file) => file.uploadingProgress == -1,
|
||||
).length;
|
||||
|
||||
if (fileErrorCount > 0) {
|
||||
if (!errorToastShown) {
|
||||
toast.error(
|
||||
t("upload.notify.count-failed", { count: fileErrorCount }),
|
||||
{
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
errorToastShown = true;
|
||||
} else {
|
||||
cleanNotifications();
|
||||
errorToastShown = false;
|
||||
}
|
||||
}, [uploadingFiles]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group position="right" mb={20}>
|
||||
<Button loading={isUploading} disabled={!dirty} onClick={() => save()}>
|
||||
<FormattedMessage id="common.button.save" />
|
||||
</Button>
|
||||
</Group>
|
||||
<Dropzone
|
||||
title={t("share.edit.append-upload")}
|
||||
maxShareSize={maxShareSize}
|
||||
showCreateUploadModalCallback={appendFiles}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{existingAndUploadedFiles.length > 0 && (
|
||||
<FileList files={existingAndUploadedFiles} setFiles={setFiles} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default EditableUpload;
|
||||
@@ -1,41 +1,106 @@
|
||||
import { ActionIcon, Table } from "@mantine/core";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
import { TbTrash } from "react-icons/tb";
|
||||
import { FileUpload } from "../../types/File.type";
|
||||
import { GrUndo } from "react-icons/gr";
|
||||
import { FileListItem } from "../../types/File.type";
|
||||
import { byteToHumanSizeString } from "../../utils/fileSize.util";
|
||||
import UploadProgressIndicator from "./UploadProgressIndicator";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const FileList = ({
|
||||
const FileListRow = ({
|
||||
file,
|
||||
onRemove,
|
||||
onRestore,
|
||||
}: {
|
||||
file: FileListItem;
|
||||
onRemove?: () => void;
|
||||
onRestore?: () => void;
|
||||
}) => {
|
||||
{
|
||||
const uploadable = "uploadingProgress" in file;
|
||||
const uploading = uploadable && file.uploadingProgress !== 0;
|
||||
const removable = uploadable
|
||||
? file.uploadingProgress === 0
|
||||
: onRemove && !file.deleted;
|
||||
const restorable = onRestore && !uploadable && !!file.deleted; // maybe undefined, force boolean
|
||||
const deleted = !uploadable && !!file.deleted;
|
||||
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
color: deleted ? "rgba(120, 120, 120, 0.5)" : "inherit",
|
||||
textDecoration: deleted ? "line-through" : "none",
|
||||
}}
|
||||
>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(+file.size)}</td>
|
||||
<td>
|
||||
{removable && (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
)}
|
||||
{uploading && (
|
||||
<UploadProgressIndicator progress={file.uploadingProgress} />
|
||||
)}
|
||||
{restorable && (
|
||||
<ActionIcon
|
||||
color="primary"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={onRestore}
|
||||
>
|
||||
<GrUndo />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const FileList = <T extends FileListItem = FileListItem>({
|
||||
files,
|
||||
setFiles,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
setFiles: Dispatch<SetStateAction<FileUpload[]>>;
|
||||
files: T[];
|
||||
setFiles: (files: T[]) => void;
|
||||
}) => {
|
||||
const remove = (index: number) => {
|
||||
files.splice(index, 1);
|
||||
const file = files[index];
|
||||
|
||||
if ("uploadingProgress" in file) {
|
||||
files.splice(index, 1);
|
||||
} else {
|
||||
files[index] = { ...file, deleted: true };
|
||||
}
|
||||
|
||||
setFiles([...files]);
|
||||
};
|
||||
|
||||
const restore = (index: number) => {
|
||||
const file = files[index];
|
||||
|
||||
if ("uploadingProgress" in file) {
|
||||
return;
|
||||
} else {
|
||||
files[index] = { ...file, deleted: false };
|
||||
}
|
||||
|
||||
setFiles([...files]);
|
||||
};
|
||||
|
||||
const rows = files.map((file, i) => (
|
||||
<tr key={i}>
|
||||
<td>{file.name}</td>
|
||||
<td>{byteToHumanSizeString(file.size)}</td>
|
||||
<td>
|
||||
{file.uploadingProgress == 0 ? (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="light"
|
||||
size={25}
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<TbTrash />
|
||||
</ActionIcon>
|
||||
) : (
|
||||
<UploadProgressIndicator progress={file.uploadingProgress} />
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<FileListRow
|
||||
key={i}
|
||||
file={file}
|
||||
onRemove={() => remove(i)}
|
||||
onRestore={() => restore(i)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||
import moment from "moment";
|
||||
import { useState } from "react";
|
||||
import { TbAlertCircle } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
@@ -38,9 +39,10 @@ const showCreateUploadModal = (
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
},
|
||||
files: FileUpload[],
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
|
||||
@@ -69,6 +71,7 @@ const CreateUploadModalBody = ({
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
@@ -89,9 +92,17 @@ const CreateUploadModalBody = ({
|
||||
.matches(new RegExp("^[a-zA-Z0-9_-]*$"), {
|
||||
message: t("upload.modal.link.error.invalid"),
|
||||
}),
|
||||
password: yup.string().min(3).max(30),
|
||||
maxViews: yup.number().min(1),
|
||||
password: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3)
|
||||
.max(30),
|
||||
maxViews: yup
|
||||
.number()
|
||||
.transform((value) => value || undefined)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
link: generatedLink,
|
||||
@@ -105,6 +116,58 @@ const CreateUploadModalBody = ({
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
if (!(await shareService.isShareIdAvailable(values.link))) {
|
||||
form.setFieldError("link", t("upload.modal.link.error.taken"));
|
||||
} else {
|
||||
const expirationString = form.values.never_expires
|
||||
? "never"
|
||||
: form.values.expiration_num + form.values.expiration_unit;
|
||||
|
||||
const expirationDate = moment().add(
|
||||
form.values.expiration_num,
|
||||
form.values.expiration_unit.replace(
|
||||
"-",
|
||||
"",
|
||||
) as moment.unitOfTime.DurationConstructor,
|
||||
);
|
||||
|
||||
if (
|
||||
options.maxExpirationInHours != 0 &&
|
||||
(form.values.never_expires ||
|
||||
expirationDate.isAfter(
|
||||
moment().add(options.maxExpirationInHours, "hours"),
|
||||
))
|
||||
) {
|
||||
form.setFieldError(
|
||||
"expiration_num",
|
||||
t("upload.modal.expires.error.too-long", {
|
||||
max: moment
|
||||
.duration(options.maxExpirationInHours, "hours")
|
||||
.humanize(),
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
uploadCallback(
|
||||
{
|
||||
id: values.link,
|
||||
expiration: expirationString,
|
||||
recipients: values.recipients,
|
||||
description: values.description,
|
||||
security: {
|
||||
password: values.password || undefined,
|
||||
maxViews: values.maxViews || undefined,
|
||||
},
|
||||
},
|
||||
files,
|
||||
);
|
||||
modals.closeAll();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||
@@ -118,33 +181,9 @@ const CreateUploadModalBody = ({
|
||||
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||
</Alert>
|
||||
)}
|
||||
<form
|
||||
onSubmit={form.onSubmit(async (values) => {
|
||||
if (!(await shareService.isShareIdAvailable(values.link))) {
|
||||
form.setFieldError("link", t("upload.modal.link.error.taken"));
|
||||
} else {
|
||||
const expiration = form.values.never_expires
|
||||
? "never"
|
||||
: form.values.expiration_num + form.values.expiration_unit;
|
||||
uploadCallback(
|
||||
{
|
||||
id: values.link,
|
||||
expiration: expiration,
|
||||
recipients: values.recipients,
|
||||
description: values.description,
|
||||
security: {
|
||||
password: values.password,
|
||||
maxViews: values.maxViews,
|
||||
},
|
||||
},
|
||||
files
|
||||
);
|
||||
modals.closeAll();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack align="stretch">
|
||||
<Group align="end">
|
||||
<Group align={form.errors.link ? "center" : "flex-end"}>
|
||||
<TextInput
|
||||
style={{ flex: "1" }}
|
||||
variant="filled"
|
||||
@@ -160,7 +199,7 @@ const CreateUploadModalBody = ({
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7)
|
||||
.substr(10, 7),
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -179,7 +218,7 @@ const CreateUploadModalBody = ({
|
||||
</Text>
|
||||
{!options.isReverseShare && (
|
||||
<>
|
||||
<Grid align={form.errors.link ? "center" : "flex-end"}>
|
||||
<Grid align={form.errors.expiration_num ? "center" : "flex-end"}>
|
||||
<Col xs={6}>
|
||||
<NumberInput
|
||||
min={1}
|
||||
@@ -243,10 +282,12 @@ const CreateUploadModalBody = ({
|
||||
/>
|
||||
</Col>
|
||||
</Grid>
|
||||
<Checkbox
|
||||
label={t("upload.modal.expires.never-long")}
|
||||
{...form.getInputProps("never_expires")}
|
||||
/>
|
||||
{options.maxExpirationInHours == 0 && (
|
||||
<Checkbox
|
||||
label={t("upload.modal.expires.never-long")}
|
||||
{...form.getInputProps("never_expires")}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
italic
|
||||
size="xs"
|
||||
@@ -259,7 +300,7 @@ const CreateUploadModalBody = ({
|
||||
neverExpires: t("upload.modal.completed.never-expires"),
|
||||
expiresOn: t("upload.modal.completed.expires-on"),
|
||||
},
|
||||
form
|
||||
form,
|
||||
)}
|
||||
</Text>
|
||||
</>
|
||||
@@ -274,7 +315,7 @@ const CreateUploadModalBody = ({
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.description.placeholder"
|
||||
"upload.modal.accordion.description.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
@@ -298,7 +339,7 @@ const CreateUploadModalBody = ({
|
||||
if (!query.match(/^\S+@\S+\.\S+$/)) {
|
||||
form.setFieldError(
|
||||
"recipients",
|
||||
t("upload.modal.accordion.email.invalid-email")
|
||||
t("upload.modal.accordion.email.invalid-email"),
|
||||
);
|
||||
} else {
|
||||
form.setFieldError("recipients", null);
|
||||
@@ -324,7 +365,7 @@ const CreateUploadModalBody = ({
|
||||
<PasswordInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.password.placeholder"
|
||||
"upload.modal.accordion.security.password.placeholder",
|
||||
)}
|
||||
label={t("upload.modal.accordion.security.password.label")}
|
||||
autoComplete="off"
|
||||
@@ -335,7 +376,7 @@ const CreateUploadModalBody = ({
|
||||
type="number"
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.security.max-views.placeholder"
|
||||
"upload.modal.accordion.security.max-views.placeholder",
|
||||
)}
|
||||
label={t("upload.modal.accordion.security.max-views.label")}
|
||||
{...form.getInputProps("maxViews")}
|
||||
|
||||
@@ -4,13 +4,17 @@ 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 italian from "./translations/it-IT";
|
||||
import japanese from "./translations/ja-JP";
|
||||
import dutch from "./translations/nl-BE";
|
||||
import polish from "./translations/pl-PL";
|
||||
import portuguese from "./translations/pt-BR";
|
||||
import russian from "./translations/ru-RU";
|
||||
import serbian from "./translations/sr-SP";
|
||||
import swedish from "./translations/sv-SE";
|
||||
import thai from "./translations/th-TH";
|
||||
import chineseSimplified from "./translations/zh-CN";
|
||||
import chineseTraditional from "./translations/zh-TW";
|
||||
|
||||
export const LOCALES = {
|
||||
ENGLISH: {
|
||||
@@ -48,6 +52,11 @@ export const LOCALES = {
|
||||
code: "zh-CN",
|
||||
messages: chineseSimplified,
|
||||
},
|
||||
CHINESE_TRADITIONAL: {
|
||||
name: "正體中文",
|
||||
code: "zh-TW",
|
||||
messages: chineseTraditional,
|
||||
},
|
||||
FINNISH: {
|
||||
name: "Suomi",
|
||||
code: "fi-FI",
|
||||
@@ -78,4 +87,19 @@ export const LOCALES = {
|
||||
code: "ja-JP",
|
||||
messages: japanese,
|
||||
},
|
||||
POLISH: {
|
||||
name: "Polski",
|
||||
code: "pl-PL",
|
||||
messages: polish,
|
||||
},
|
||||
SWEDISH: {
|
||||
name: "Svenska",
|
||||
code: "sv-SE",
|
||||
messages: swedish,
|
||||
},
|
||||
ITALIAN: {
|
||||
name: "Italiano",
|
||||
code: "it-IT",
|
||||
messages: italian,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"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",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Opret en bruger",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Din e-mail",
|
||||
"signup.button.submit": "Lad os komme i gang",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Log ind",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Glemt din adgangskode?",
|
||||
"resetPassword.description": "Indtast din e-mail for at nulstille din adgangskode.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Adgangskode",
|
||||
"account.card.password.old": "Gammel adgangskode",
|
||||
"account.card.password.new": "Ny adgangskode",
|
||||
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "Adgangskoden er ændret",
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
"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": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"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.expires.error.too-long": "Udløbsdatoen overskrider den maksimalt tilladte udløbsdato på {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Udløb",
|
||||
"upload.modal.expires.minute-singular": "Minut",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Social Login",
|
||||
"admin.config.general.app-name": "App-navn",
|
||||
"admin.config.general.app-name.description": "Navnet på applikationen",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"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-expiration": "Maks. udløb",
|
||||
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"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",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Adgangskode",
|
||||
"admin.config.smtp.password.description": "Adgangskoden til SMTP serveren",
|
||||
"admin.config.smtp.button.test": "Send test e-mail",
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Ups! Denne side findes ikke.",
|
||||
"404.button.home": "Gå tilbage",
|
||||
// error
|
||||
"error.title": "Fejl",
|
||||
"error.description": "Hovsa!",
|
||||
"error.button.back": "Gå tilbage",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "Du har ikke tilladelse til at logge ind.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Gem",
|
||||
"common.button.create": "Opret",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"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",
|
||||
"signIn.oauth.or": "ODER",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Erstelle ein Konto",
|
||||
@@ -44,10 +50,14 @@ export default {
|
||||
"signup.input.email.placeholder": "Deine Emailadresse",
|
||||
"signup.button.submit": "Lass uns loslegen",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentifizierung",
|
||||
"totp.button.signIn": "Anmelden",
|
||||
// END /auth/totp
|
||||
// /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.notify.success": "Ein Link zum Zurü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",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Passwort",
|
||||
"account.card.password.old": "Altes Passwort",
|
||||
"account.card.password.new": "Neues Passwort",
|
||||
"account.card.password.noPasswordSet": "Du hast kein Passwort erstellt. Wenn Du Dich mit E-Mail und Passwort anmelden möchtest, musst Du ein Passwort festlegen.",
|
||||
"account.notify.password.success": "Passwort erfolgreich geändert",
|
||||
"account.card.oauth.title": "Anmeldung über soziale Netzwerke",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Verknüpfen",
|
||||
"account.card.oauth.unlink": "Verknüpfung aufheben",
|
||||
"account.card.oauth.unlinked": "Verknüpfung aufgehoben",
|
||||
"account.modal.unlink.title": "Kontoverknüpfung aufheben",
|
||||
"account.modal.unlink.description": "Das Entfernen der Verknüpfung mit Deinem sozialen Konten kann dazu führen, dass Du Dein Konto verlierst, wenn Du Dich nicht an Deinen Benutzernamen und Dein Passwort erinnerst.",
|
||||
"account.notify.oauth.unlinked.success": "Verknüpfung erfolgreich aufgehoben",
|
||||
"account.card.security.title": "Sicherheit",
|
||||
"account.card.security.totp.enable.description": "Gib dein aktuelles Passwort ein, um TOTP zu aktivieren",
|
||||
"account.card.security.totp.disable.description": "Gib dein aktuelles Passwort ein, um TOTP zu deaktivieren",
|
||||
@@ -90,9 +113,9 @@ export default {
|
||||
// /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.description.empty": "Du hast keine Freigaben erstellt.",
|
||||
"account.shares.button.create": "Erstelle eine",
|
||||
"account.shares.info.title": "Teile deine Information",
|
||||
"account.shares.info.title": "Freigabe Informationen",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Name",
|
||||
"account.shares.table.description": "Beschreibung",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"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.expires.error.too-long": "Ablauf überschreitet das maximale Ablaufdatum von {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Gültig bis",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"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 lade die Datei herunter, um sie anzuzeigen.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "{shareId} bearbeiten",
|
||||
"share.edit.append-upload": "Datei anfügen",
|
||||
"share.edit.notify.generic-error": "Während der Erstellung der Freigabe ist ein Fehler aufgetreten.",
|
||||
"share.edit.notify.save-success": "Freigabe erfolgreich aktualisiert",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Anmeldung über soziale Netzwerke",
|
||||
"admin.config.general.app-name": "App-Name",
|
||||
"admin.config.general.app-name.description": "Name der Applikation",
|
||||
"admin.config.general.app-url": "App-URL",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"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-expiration": "Max. Ablaufdatum",
|
||||
"admin.config.share.max-expiration.description": "Maximale Ablaufzeit in Stunden. Auf 0 setzen, um kein Ablaufdatum zu definieren.",
|
||||
"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",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Passwort",
|
||||
"admin.config.smtp.password.description": "Passwort des SMTP-Servers",
|
||||
"admin.config.smtp.button.test": "Test-E-Mail senden",
|
||||
"admin.config.oauth.allow-registration": "Registrierung erlauben",
|
||||
"admin.config.oauth.allow-registration.description": "Benutzern erlauben, sich über Soziale Netzwerke zu registrieren",
|
||||
"admin.config.oauth.ignore-totp": "TOTP ignorieren",
|
||||
"admin.config.oauth.ignore-totp.description": "Gibt an, ob TOTP ignoriert werden soll, wenn sich der Benutzer über Soziale Netzwerke anmeldet",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "GitHub Anmeldung erlaubt",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client-ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client-ID der GitHub OAuth-App",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client-Secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client-Secret der GitHub OAuth-App",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Google Anmeldung erlaubt",
|
||||
"admin.config.oauth.google-client-id": "Google Client-ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client-ID der Google OAuth-App",
|
||||
"admin.config.oauth.google-client-secret": "Google Client-Secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client-Secret der Google OAuth-App",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Microsoft Anmeldung erlaubt",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Mandant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Mandant-ID der Microsoft OAuth App\ncommon: Benutzer mit einem persönlichen Microsoft-Konto und einem Arbeits- oder Schulkonto von Microsoft Entra ID können sich in der Anwendung anmelden. organizations: Nur Benutzer mit Arbeits- oder Schulkonten von Microsoft Entra ID können sich in der Anwendung anmelden.\nconsumers: Nur Benutzer mit einem persönlichen Microsoft-Konto können sich in der Anwendung anmelden.\nDomänenname des Microsoft Entra Mandanten oder die Mandanten-ID im GUID-Format: Nur Benutzer eines bestimmten Microsoft Entra Mandanten (Verzeichnismitglieder mit einem Arbeits- oder Schulkonto oder Verzeichnis Gäste mit einem persönlichen Microsoft-Konto) können sich anmelden.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client-ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client-ID der Microsoft OAuth-App",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client-Secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client-Secret der Microsoft OAuth-App",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Discord Anmeldung erlaubt",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client-ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client-ID der Discord OAuth-App",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client-Secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client-Secret der Discord OAuth-App",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "OpenID Connect Anmeldung erlaubt",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client-ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client-ID der OpenID Connect OAuth-App",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client-Secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client-Secret der OpenID Connect OAuth-App",
|
||||
// 404
|
||||
"404.description": "Ups, diese Seite existiert nicht.",
|
||||
"404.button.home": "Zurück zur Startseite",
|
||||
// error
|
||||
"error.title": "Fehler",
|
||||
"error.description": "Ups!",
|
||||
"error.button.back": "Zurück",
|
||||
"error.msg.default": "Etwas ist schief gelaufen.",
|
||||
"error.msg.access_denied": "Du hast den Authentifizierungsprozess abgebrochen, bitte versuche es erneut.",
|
||||
"error.msg.expired_token": "Der Authentifizierungsprozess hat zu lange gedauert, bitte versuche es erneut.",
|
||||
"error.msg.invalid_token": "Interner Fehler",
|
||||
"error.msg.no_user": "Der mit diesem {0} Konto verknüpfte Benutzer existiert nicht.",
|
||||
"error.msg.no_email": "Kann die E-Mail-Adresse von dem Konto {0} nicht abrufen.",
|
||||
"error.msg.already_linked": "Das Konto {0} ist bereits mit einem anderen Konto verknüpft.",
|
||||
"error.msg.not_linked": "Das Konto {0} wurde noch nicht mit einem Konto verknüpft.",
|
||||
"error.msg.unverified_account": "Dieses Konto {0} wurde noch nicht verifiziert, bitte versuchen Sie es nach der Verifikation erneut.",
|
||||
"error.msg.discord_guild_permission_denied": "Du bist nicht berechtigt, Dich anzumelden.",
|
||||
"error.msg.cannot_get_user_info": "Deine Benutzerinformationen können nicht von diesem Konto {0} abgerufen werden.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Speichern",
|
||||
"common.button.create": "Erstellen",
|
||||
|
||||
@@ -43,6 +43,12 @@ export default {
|
||||
"signIn.notify.totp-required.title": "Two-factor authentication required",
|
||||
"signIn.notify.totp-required.description":
|
||||
"Please enter your two-factor authentication code",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
|
||||
// END /auth/signin
|
||||
|
||||
@@ -58,6 +64,12 @@ export default {
|
||||
|
||||
// END /auth/signup
|
||||
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Sign in",
|
||||
|
||||
// END /auth/totp
|
||||
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Forgot your password?",
|
||||
"resetPassword.description": "Enter your email to reset your password.",
|
||||
@@ -81,8 +93,23 @@ export default {
|
||||
"account.card.password.title": "Password",
|
||||
"account.card.password.old": "Old password",
|
||||
"account.card.password.new": "New password",
|
||||
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "Password changed successfully",
|
||||
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
|
||||
|
||||
"account.card.security.title": "Security",
|
||||
"account.card.security.totp.enable.description":
|
||||
"Enter your current password to start enabling TOTP",
|
||||
@@ -261,6 +288,7 @@ export default {
|
||||
|
||||
"upload.modal.expires.never": "never",
|
||||
"upload.modal.expires.never-long": "Never Expires",
|
||||
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
|
||||
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Expiration",
|
||||
@@ -330,12 +358,20 @@ export default {
|
||||
|
||||
// END /share/[id]
|
||||
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
|
||||
// /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.category.oauth": "Social Login",
|
||||
|
||||
"admin.config.general.app-name": "App name",
|
||||
"admin.config.general.app-name.description": "Name of the application",
|
||||
@@ -385,6 +421,9 @@ export default {
|
||||
"Allow unauthenticated shares",
|
||||
"admin.config.share.allow-unauthenticated-shares.description":
|
||||
"Whether unauthenticated users can create shares",
|
||||
"admin.config.share.max-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description":
|
||||
"Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"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",
|
||||
@@ -407,10 +446,74 @@ export default {
|
||||
"admin.config.smtp.password.description": "Password of the SMTP server",
|
||||
"admin.config.smtp.button.test": "Send test email",
|
||||
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
|
||||
// 404
|
||||
"404.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring me back home",
|
||||
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Go back",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
|
||||
// Common translations
|
||||
"common.button.save": "Save",
|
||||
"common.button.create": "Create",
|
||||
@@ -423,6 +526,7 @@ export default {
|
||||
"common.button.generate": "Generate",
|
||||
"common.button.done": "Done",
|
||||
"common.text.link": "Link",
|
||||
"common.text.navigate-to-link": "Go to the link",
|
||||
"common.text.or": "or",
|
||||
"common.button.go-back": "Go back",
|
||||
"common.notify.copied": "Your link was copied to the clipboard",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"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",
|
||||
"signIn.oauth.or": "O",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Crear una cuenta",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Tu correo",
|
||||
"signup.button.submit": "Comencemos",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Autenticación TOTP",
|
||||
"totp.button.signIn": "Iniciar sesión",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "¿Olvidaste tu contraseña?",
|
||||
"resetPassword.description": "Ingresa tu correo para restablecer tu contraseña.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Contraseña",
|
||||
"account.card.password.old": "Anterior contraseña",
|
||||
"account.card.password.new": "Nueva contraseña",
|
||||
"account.card.password.noPasswordSet": "No tienes una establecida contraseña. Si deseas iniciar sesión con correo electrónico y una contraseña necesitas crear una contraseña.",
|
||||
"account.notify.password.success": "Contraseña cambiada correctamente",
|
||||
"account.card.oauth.title": "Inicio de sesión con red social",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Vincular",
|
||||
"account.card.oauth.unlink": "Desvincular",
|
||||
"account.card.oauth.unlinked": "Desvinculado",
|
||||
"account.modal.unlink.title": "Desvincular cuenta",
|
||||
"account.modal.unlink.description": "Desvincular tus cuentas sociales puede causar que pierdas tu cuenta si no recuerdas tu nombre de usuario y contraseña.",
|
||||
"account.notify.oauth.unlinked.success": "Desvinculado 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",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"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.expires.error.too-long": "La caducidad excede la fecha de caducidad máxima de {max}.",
|
||||
"upload.modal.link.label": "Enlace",
|
||||
"upload.modal.expires.label": "Expiración",
|
||||
"upload.modal.expires.minute-singular": "Minuto",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Editar {shareId}",
|
||||
"share.edit.append-upload": "Agregar archivo",
|
||||
"share.edit.notify.generic-error": "Ha ocurrido un error mientras se compartía tu archivo.",
|
||||
"share.edit.notify.save-success": "Compartir actualizado correctamente",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Inicio de sesión social",
|
||||
"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",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"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-expiration": "Expiración máxima",
|
||||
"admin.config.share.max-expiration.description": "Expiración máxima para compartir en horas. Establezca en 0 para permitir una expiración ilimitada.",
|
||||
"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",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"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",
|
||||
"admin.config.oauth.allow-registration": "Permitir registro",
|
||||
"admin.config.oauth.allow-registration.description": "Permitir a los usuarios registrarse mediante login social",
|
||||
"admin.config.oauth.ignore-totp": "Ignorar TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Ignorar TOTP cuando el usuario utiliza inicio de sesión social",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Si el inicio de sesión de GitHub está habilitado",
|
||||
"admin.config.oauth.github-client-id": "ID del Cliente de GitHub",
|
||||
"admin.config.oauth.github-client-id.description": "ID de cliente de la aplicación OAuth de GitHub",
|
||||
"admin.config.oauth.github-client-secret": "Palabra secreta del Cliente de GitHub",
|
||||
"admin.config.oauth.github-client-secret.description": "Secreto de cliente de la aplicación OAuth de GitHub",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Si el inicio de sesión de Google está habilitado",
|
||||
"admin.config.oauth.google-client-id": "ID del Cliente de Google",
|
||||
"admin.config.oauth.google-client-id.description": "ID de cliente de la aplicación de Google OAuth",
|
||||
"admin.config.oauth.google-client-secret": "Palabra secreta del cliente de Google",
|
||||
"admin.config.oauth.google-client-secret.description": "Secreto del cliente de la aplicación Google OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Si el inicio de sesión de Microsoft está habilitado",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "ID del cliente de Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description": "ID de cliente de la app OAuth de Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret": "Secreto del cliente de Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Oops esta página no existe.",
|
||||
"404.button.home": "Regrésame al inicio",
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "¡Ups!",
|
||||
"error.button.back": "Volver",
|
||||
"error.msg.default": "Algo ha salido mal.",
|
||||
"error.msg.access_denied": "Ha cancelado el proceso de autenticación, por favor inténtelo de nuevo.",
|
||||
"error.msg.expired_token": "El proceso de autenticación ha tardado demasiado, por favor inténtelo de nuevo.",
|
||||
"error.msg.invalid_token": "Error interno",
|
||||
"error.msg.no_user": "El usuario vinculado a esta cuenta {0} no existe.",
|
||||
"error.msg.no_email": "No se puede obtener la dirección de correo electrónico de esta cuenta {0}.",
|
||||
"error.msg.already_linked": "Esta cuenta {0} ya está vinculada a otra cuenta.",
|
||||
"error.msg.not_linked": "Esta cuenta {0} aún no ha sido vinculada a ninguna cuenta.",
|
||||
"error.msg.unverified_account": "Esta cuenta {0} no está verificada, por favor inténtalo de nuevo después de la verificación.",
|
||||
"error.msg.discord_guild_permission_denied": "No tienes permitido iniciar sesion.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Guardar",
|
||||
"common.button.create": "Crear",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"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",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Rekisteröidy",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Sähköpostisi",
|
||||
"signup.button.submit": "Aloitetaan",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Sign in",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Unohditko salasanan?",
|
||||
"resetPassword.description": "Kirjoita sähköpostiosoitteesi palauttaaksesi salasanasi.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Salasana",
|
||||
"account.card.password.old": "Vanha salasana",
|
||||
"account.card.password.new": "Uusi salasana",
|
||||
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "Salasana vaihdettu",
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
"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ä",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"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.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
|
||||
"upload.modal.link.label": "Linkki",
|
||||
"upload.modal.expires.label": "Vanhentuminen",
|
||||
"upload.modal.expires.minute-singular": "Minuutti",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Social Login",
|
||||
"admin.config.general.app-name": "Sovelluksen nimi",
|
||||
"admin.config.general.app-name.description": "Sovelluksen nimi",
|
||||
"admin.config.general.app-url": "Sovelluksen URL",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"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-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"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",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Salasana",
|
||||
"admin.config.smtp.password.description": "SMTP palvelimen salasana",
|
||||
"admin.config.smtp.button.test": "Lähetä testisähköposti",
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Hups tätä sivua ei ole olemassa.",
|
||||
"404.button.home": "Tuo minut takaisin kotiin",
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Go back",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Tallenna",
|
||||
"common.button.create": "Luo",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Envoyer",
|
||||
"navbar.upload": "Téléverser",
|
||||
"navbar.signin": "Se connecter",
|
||||
"navbar.home": "Accueil",
|
||||
"navbar.signup": "S'inscrire",
|
||||
"navbar.signup": "S’inscrire",
|
||||
"navbar.links.shares": "Mes partages",
|
||||
"navbar.links.reverse": "Partages inversés",
|
||||
"navbar.avatar.account": "Mon compte",
|
||||
@@ -18,36 +18,46 @@ export default {
|
||||
"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.bullet.c.description": "Téléchargez des fichiers aussi 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.button.signup": "S’inscrire",
|
||||
"signin.input.email-or-username": "Courriel ou surnom",
|
||||
"signin.input.email-or-username.placeholder": "Votre courriel ou surnom",
|
||||
"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",
|
||||
"signIn.oauth.or": "OU",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// 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.username": "Surnom",
|
||||
"signup.input.username.placeholder": "Votre surnom",
|
||||
"signup.input.email": "Adresse email",
|
||||
"signup.input.email.placeholder": "Votre adresse email",
|
||||
"signup.input.email.placeholder": "Votre courriel",
|
||||
"signup.button.submit": "Commençons",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Authentification TOTP",
|
||||
"totp.button.signIn": "Se connecter",
|
||||
// END /auth/totp
|
||||
// /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.description": "Saisissez votre courriel pour réinitialiser votre mot de passe.",
|
||||
"resetPassword.notify.success": "Un courriel 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",
|
||||
@@ -56,17 +66,30 @@ export default {
|
||||
// /account
|
||||
"account.title": "Mon compte",
|
||||
"account.card.info.title": "Détails du compte",
|
||||
"account.card.info.username": "Pseudo",
|
||||
"account.card.info.username": "Surnom",
|
||||
"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.card.password.noPasswordSet": "Vous n’avez pas de mot de passe défini. Si vous voulez vous connecter avec un e-mail et un mot de passe, vous devez définir un mot de passe.",
|
||||
"account.notify.password.success": "Le mot de passe a été modifié avec succès",
|
||||
"account.card.oauth.title": "Identifiant social",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Associer",
|
||||
"account.card.oauth.unlink": "Dissocier",
|
||||
"account.card.oauth.unlinked": "Dissocié",
|
||||
"account.modal.unlink.title": "Dissocier le compte",
|
||||
"account.modal.unlink.description": "Dissocier vos comptes de réseaux sociaux peut vous faire perdre votre compte si jamais vous ne vous souvenez pas de votre nom d’utilisateur et de votre mot de passe.",
|
||||
"account.notify.oauth.unlinked.success": "Dissocié 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.card.security.totp.enable.description": "Entrez votre mot de passe actuel pour activer TOTP",
|
||||
"account.card.security.totp.disable.description": "Entrez votre mot de passe actuel pour désactiver TOTP",
|
||||
"account.card.security.totp.button.start": "Démarrer",
|
||||
"account.modal.totp.title": "Activer TOTP",
|
||||
"account.modal.totp.step1": "Étape 1 : Ajouter votre authentificateur",
|
||||
"account.modal.totp.step2": "Étape 2 : Valider votre code",
|
||||
@@ -74,8 +97,8 @@ export default {
|
||||
"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.notify.totp.disable": "TOTP désactivé avec succès",
|
||||
"account.notify.totp.enable": "TOTP activé avec succès",
|
||||
"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",
|
||||
@@ -108,8 +131,8 @@ export default {
|
||||
// /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é.",
|
||||
"account.reverseShares.title.empty": "C’est plutôt vide 👀",
|
||||
"account.reverseShares.description.empty": "Vous n’avez aucun partage inversé.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Créer un partage inversé",
|
||||
"account.reverseShares.modal.expiration.label": "Expiration",
|
||||
@@ -126,8 +149,8 @@ export default {
|
||||
"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.send-email": "Envoyer un courriel de notification",
|
||||
"account.reverseShares.modal.send-email.description": "Envoyer une notification par courriel 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.",
|
||||
@@ -138,10 +161,10 @@ export default {
|
||||
"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.table.expires": "Expire le",
|
||||
"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.",
|
||||
"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",
|
||||
@@ -151,11 +174,11 @@ export default {
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Gestion des utilisateurs",
|
||||
"admin.users.table.username": "Pseudo",
|
||||
"admin.users.table.email": "Adresse email",
|
||||
"admin.users.table.username": "Surnom",
|
||||
"admin.users.table.email": "Courriel",
|
||||
"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.admin-privileges": "Privilèges d’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",
|
||||
@@ -165,17 +188,17 @@ export default {
|
||||
// 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.email": "Courriel",
|
||||
"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.",
|
||||
"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 d’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.",
|
||||
"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.",
|
||||
@@ -190,7 +213,8 @@ export default {
|
||||
"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.expires.never-long": "N’expire jamais",
|
||||
"upload.modal.expires.error.too-long": "L’expiration dépasse la date d'expiration du {max}.",
|
||||
"upload.modal.link.label": "Lien",
|
||||
"upload.modal.expires.label": "Expiration",
|
||||
"upload.modal.expires.minute-singular": "Minute",
|
||||
@@ -209,25 +233,25 @@ export default {
|
||||
"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.email.invalid-email": "Courriel 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.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.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.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",
|
||||
@@ -240,63 +264,134 @@ export default {
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Modifier {shareId}",
|
||||
"share.edit.append-upload": "Ajouter un fichier",
|
||||
"share.edit.notify.generic-error": "Une erreur est survenue durant le traitement de votre partage.",
|
||||
"share.edit.notify.save-success": "Partage mis à jour avec succès",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Identifiant social",
|
||||
"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.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.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.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 d’un 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.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 d’une 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.allow-unauthenticated-shares.description": "Permet aux visiteurs de créer des partages",
|
||||
"admin.config.share.max-expiration": "Échéance",
|
||||
"admin.config.share.max-expiration.description": "Échéance du partage en heures. Indiquez 0 pour qu’il n’expire jamais.",
|
||||
"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.enabled.description": "Active SMTP. Activez ceci uniquement si vous avez saisi l’hôte, le port, le courriel, l’utilisateur et son 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.email": "Courriel",
|
||||
"admin.config.smtp.email.description": "Adresse à partir de laquelle les courriels 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",
|
||||
"admin.config.smtp.button.test": "Envoyer un courriel de test",
|
||||
"admin.config.oauth.allow-registration": "Autoriser l’inscription",
|
||||
"admin.config.oauth.allow-registration.description": "Permettre aux utilisateurs de s’inscrire via leur identifiant social",
|
||||
"admin.config.oauth.ignore-totp": "Ignorer TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Ignorer le TOTP lorsque l’utilisateur utilise un identifiant social.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Permettre la connexion via GitHub.",
|
||||
"admin.config.oauth.github-client-id": "ID client de GitHub",
|
||||
"admin.config.oauth.github-client-id.description": "L’ID du client de l’application OAuth GitHub",
|
||||
"admin.config.oauth.github-client-secret": "Secret du client GitHub",
|
||||
"admin.config.oauth.github-client-secret.description": "Le secret du client de l’application OAuth GitHub",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Permettre la connexion via Google.",
|
||||
"admin.config.oauth.google-client-id": "ID du client Google",
|
||||
"admin.config.oauth.google-client-id.description": "L’ID du client de l’application OAuth Google",
|
||||
"admin.config.oauth.google-client-secret": "Secret client de Google",
|
||||
"admin.config.oauth.google-client-secret.description": "Le secret du client de l’application OAuth Google",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Permettre la connexion via Microsoft.",
|
||||
"admin.config.oauth.microsoft-tenant": "Locataire Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant.description": "ID locataire de l'application OAuth Microsoft\nCommun : les utilisateurs ayant au choix un compte personnel ou d’entreprise issue de Microsoft Entra.\nOrganisations : limité aux utilisateurs ayant un compte d’entreprise ou d’université issue de Microsoft Entra.\nPersonnel : limité aux utilisateurs ayant un compte personnel\nDomanial : limité aux utilisateurs d'un domaine Microsoft Entra spécifié ou d’un ID locataire (au format GUID), qu’ils soient membres d’un registre d’entreprise ou d’université ou bien enregistrés en tant qu’invités avec un compte personnel.",
|
||||
"admin.config.oauth.microsoft-client-id": "ID du client Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description": "L’ID du client de l’application Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Secret du client Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Le secret du client de l’application Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Permettre la connexion via Discord.",
|
||||
"admin.config.oauth.discord-limited-guild": "ID de serveur restreint Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limiter la connexion aux utilisateurs sur un serveur spécifique. Laissez vide pour désactiver.",
|
||||
"admin.config.oauth.discord-client-id": "ID du client Discord",
|
||||
"admin.config.oauth.discord-client-id.description": "L’ID du client de l’application OAuth Discord",
|
||||
"admin.config.oauth.discord-client-secret": "Secret du client Discord",
|
||||
"admin.config.oauth.discord-client-secret.description": "Le secret du client de l’application OAuth Discord",
|
||||
"admin.config.oauth.oidc-enabled": "Connexion OpenID",
|
||||
"admin.config.oauth.oidc-enabled.description": "Permettre la connexion via OpenID",
|
||||
"admin.config.oauth.oidc-discovery-uri": "URI de découverte OpenID",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "L’URI de découverte de la connexion à l'application OpenID OAuth",
|
||||
"admin.config.oauth.oidc-username-claim": "Revendication du nom d’utilisateur OpenID",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Le champ contenant la revendication du nom d’utilisateur dans le jeton OpenID Connect. Laissez vide si vous ne savez pas quoi indiquer.",
|
||||
"admin.config.oauth.oidc-client-id": "ID du client OpenID",
|
||||
"admin.config.oauth.oidc-client-id.description": "L’ID du client de l’application OAuth OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-secret": "Secret du client OpenID",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Le secret du client de l’application OAuth OpenID Connect",
|
||||
// 404
|
||||
"404.description": "Désolé, mais cette page n’existe pas.",
|
||||
"404.button.home": "Retour à l'accueil",
|
||||
"404.button.home": "Retour à l’accueil",
|
||||
// error
|
||||
"error.title": "Erreur",
|
||||
"error.description": "Oups !",
|
||||
"error.button.back": "Retour",
|
||||
"error.msg.default": "Quelque chose a mal tourné.",
|
||||
"error.msg.access_denied": "Vous avez annulé le processus d’authentification, veuillez réessayer.",
|
||||
"error.msg.expired_token": "Le processus d’authentification a pris trop de temps, veuillez réessayer.",
|
||||
"error.msg.invalid_token": "Erreur Interne",
|
||||
"error.msg.no_user": "L’utilisateur associé au compte {0} n’existe pas.",
|
||||
"error.msg.no_email": "Impossible d’obtenir le courriel du compte {0}.",
|
||||
"error.msg.already_linked": "Le compte {0} est déjà associé à un autre compte.",
|
||||
"error.msg.not_linked": "Le compte {0} n’est pas encore associé à compte.",
|
||||
"error.msg.unverified_account": "Le compte {0} n'est pas vérifié, veuillez réessayer après vérification.",
|
||||
"error.msg.discord_guild_permission_denied": "Vous n’êtes pas autorisé à vous authentifier.",
|
||||
"error.msg.cannot_get_user_info": "Impossible d’obtenir vos informations utilisateur à partir du compte {0}.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "Connexion OpenID",
|
||||
// Common translations
|
||||
"common.button.save": "Sauvegarder",
|
||||
"common.button.create": "Créer",
|
||||
|
||||
419
frontend/src/i18n/translations/it-IT.ts
Normal file
419
frontend/src/i18n/translations/it-IT.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Carica",
|
||||
"navbar.signin": "Registrati",
|
||||
"navbar.home": "Home",
|
||||
"navbar.signup": "Registrati",
|
||||
"navbar.links.shares": "Le mie condivisioni",
|
||||
"navbar.links.reverse": "Reverse shares",
|
||||
"navbar.avatar.account": "Il mio account",
|
||||
"navbar.avatar.admin": "Amministrazione",
|
||||
"navbar.avatar.signout": "Disconetti",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "Una piattaforma di condivisione di file <h>autohosted</h>.",
|
||||
"home.description": "Vuoi davvero dare i tuoi file personali in mano a terzi come WeTransfer?",
|
||||
"home.bullet.a.name": "Self-hosted",
|
||||
"home.bullet.a.description": "Ospita Pingvin Condividi sulla tua macchina.",
|
||||
"home.bullet.b.name": "Privacy",
|
||||
"home.bullet.b.description": "I tuoi file sono i tuoi file e non dovrebbero mai entrare nelle mani di terzi.",
|
||||
"home.bullet.c.name": "Nessun fastidioso limite alle dimensioni dei files",
|
||||
"home.bullet.c.description": "Carica file grandi quanto vuoi. Solo il tuo disco rigido sarà il tuo limite.",
|
||||
"home.button.start": "Inizia adesso",
|
||||
"home.button.source": "Codice sorgente",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Bentornato",
|
||||
"signin.description": "Non hai ancora un account?",
|
||||
"signin.button.signup": "Registrazione",
|
||||
"signin.input.email-or-username": "Email o nome utente",
|
||||
"signin.input.email-or-username.placeholder": "La tua email o nome utente",
|
||||
"signin.input.password": "Password",
|
||||
"signin.input.password.placeholder": "Password",
|
||||
"signin.button.submit": "Accedi",
|
||||
"signIn.notify.totp-required.title": "Autenticazione a due fattori richiesta",
|
||||
"signIn.notify.totp-required.description": "Inserisci il tuo codice di autenticazione a due fattori",
|
||||
"signIn.oauth.or": "OPPURE",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Crea un account",
|
||||
"signup.description": "Hai già un account?",
|
||||
"signup.button.signin": "Accedi",
|
||||
"signup.input.username": "Nome utente",
|
||||
"signup.input.username.placeholder": "Il tuo nome utente",
|
||||
"signup.input.email": "Email",
|
||||
"signup.input.email.placeholder": "La tua email",
|
||||
"signup.button.submit": "Iniziamo",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Autenticazione TOTP",
|
||||
"totp.button.signIn": "Accedi",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Hai dimenticato la password?",
|
||||
"resetPassword.description": "Inserisci la tua email per reimpostare la password.",
|
||||
"resetPassword.notify.success": "Abbiamo inviato un'e-mail all'indirizzo da te indicato. Clicca sul link contenuto nell'e-mail per reimpostare la password del tuo account.",
|
||||
"resetPassword.button.back": "Torna alla pagina di login",
|
||||
"resetPassword.text.resetPassword": "Reimposta password",
|
||||
"resetPassword.text.enterNewPassword": "Inserisci la tua nuova password",
|
||||
"resetPassword.input.password": "Nuova password",
|
||||
"resetPassword.notify.passwordReset": "La password è stata reimpostata correttamente.",
|
||||
// /account
|
||||
"account.title": "Il mio account",
|
||||
"account.card.info.title": "Informazioni sull'account",
|
||||
"account.card.info.username": "Nome utente",
|
||||
"account.card.info.email": "Email",
|
||||
"account.notify.info.success": "Account aggiornato correttamente",
|
||||
"account.card.password.title": "Password",
|
||||
"account.card.password.old": "Vecchia password",
|
||||
"account.card.password.new": "Nuova password",
|
||||
"account.card.password.noPasswordSet": "Non hai una password impostata. Se vuoi accedere con email e password devi impostare una password.",
|
||||
"account.notify.password.success": "Password cambiata con successo",
|
||||
"account.card.oauth.title": "Effettua il login con i Social",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Collegamento",
|
||||
"account.card.oauth.unlink": "Scollega",
|
||||
"account.card.oauth.unlinked": "Scollegato",
|
||||
"account.modal.unlink.title": "Scollega account",
|
||||
"account.modal.unlink.description": "Scollegare i tuoi account social potrebbe causare la perdita del tuo account se non ricordi il tuo nome utente e la password.",
|
||||
"account.notify.oauth.unlinked.success": "Disconnessione Riuscita",
|
||||
"account.card.security.title": "Sicurezza",
|
||||
"account.card.security.totp.enable.description": "Inserisci la tua password attuale per iniziare ad abilitare TOTP",
|
||||
"account.card.security.totp.disable.description": "Inserisci la tua password attuale per iniziare ad abilitare TOTP",
|
||||
"account.card.security.totp.button.start": "Iniziare",
|
||||
"account.modal.totp.title": "Abilita TOTP",
|
||||
"account.modal.totp.step1": "Passo 1: Aggiungi il tuo autenticatore",
|
||||
"account.modal.totp.step2": "Passo 2: Convalida il tuo codice",
|
||||
"account.modal.totp.enterManually": "Inserisci manualmente",
|
||||
"account.modal.totp.code": "Codice",
|
||||
"account.modal.totp.clickToCopy": "Clicca per copiare",
|
||||
"account.modal.totp.verify": "Verifica",
|
||||
"account.notify.totp.disable": "TOTP disabilitato con successo",
|
||||
"account.notify.totp.enable": "TOTP abilitato con successo",
|
||||
"account.card.language.title": "Lingua",
|
||||
"account.card.language.description": "Il progetto è tradotto dalla community. Alcune lingue potrebbero essere incomplete.",
|
||||
"account.card.color.title": "Schema di colori",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Scuro",
|
||||
"account.theme.light": "Chiaro",
|
||||
"account.theme.system": "Sistema",
|
||||
"account.button.delete": "Elimina account",
|
||||
"account.modal.delete.title": "Cancella Account",
|
||||
"account.modal.delete.description": "Vuoi davvero eliminare il tuo account, incluse tutte le tue condivisioni attive?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Le mie condivisioni",
|
||||
"account.shares.title.empty": "È vuoto qui 👀",
|
||||
"account.shares.description.empty": "Non hai nessuna condivisione.",
|
||||
"account.shares.button.create": "Creane uno",
|
||||
"account.shares.info.title": "Condividi le informazioni",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Nome",
|
||||
"account.shares.table.description": "Descrizione",
|
||||
"account.shares.table.visitors": "Visitatori",
|
||||
"account.shares.table.expiresAt": "Scadenza",
|
||||
"account.shares.table.createdAt": "Creato il",
|
||||
"account.shares.table.size": "Dimensione",
|
||||
"account.shares.modal.share-informations": "Condividi le informazioni",
|
||||
"account.shares.modal.share-link": "Condividi link",
|
||||
"account.shares.modal.delete.title": "Elimina condivisione {share}",
|
||||
"account.shares.modal.delete.description": "Vuoi davvero cancellare questa condivisione?",
|
||||
// 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.title": "Create reverse share",
|
||||
"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.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Social Login",
|
||||
"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-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"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",
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring me back home",
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Go back",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// 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"
|
||||
};
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
"navbar.home": "ホーム",
|
||||
"navbar.signup": "会員登録",
|
||||
"navbar.links.shares": "自分の共有",
|
||||
"navbar.links.reverse": "自分と共有",
|
||||
"navbar.links.reverse": "ファイルリクエスト",
|
||||
"navbar.avatar.account": "マイアカウント",
|
||||
"navbar.avatar.admin": "管理画面",
|
||||
"navbar.avatar.signout": "サインアウト",
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "サインイン",
|
||||
"signIn.notify.totp-required.title": "二段階認証が必要です",
|
||||
"signIn.notify.totp-required.description": "二段階認証コードを入力してください",
|
||||
"signIn.oauth.or": "または",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "アカウントを作成",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "あなたのメールアドレス",
|
||||
"signup.button.submit": "さあ始めましょう",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "二段階認証",
|
||||
"totp.button.signIn": "サインイン",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "パスワードを忘れてしまいましたか?",
|
||||
"resetPassword.description": "登録しているメールアドレスを入力してください。",
|
||||
@@ -52,273 +62,358 @@ export default {
|
||||
"resetPassword.text.resetPassword": "パスワードをリセット",
|
||||
"resetPassword.text.enterNewPassword": "新規パスワードを入力",
|
||||
"resetPassword.input.password": "新規パスワード",
|
||||
"resetPassword.notify.passwordReset": "Your password has been reset successfully.",
|
||||
"resetPassword.notify.passwordReset": "パスワードのリセットに成功しました。",
|
||||
// /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",
|
||||
"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.card.password.noPasswordSet": "パスワードが設定されていません。メールアドレスとパスワードでサインインしたい場合は、パスワードの設定が必要です。",
|
||||
"account.notify.password.success": "パスワードの変更に成功しました",
|
||||
"account.card.oauth.title": "ソーシャルログイン",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "リンク",
|
||||
"account.card.oauth.unlink": "リンク解除",
|
||||
"account.card.oauth.unlinked": "リンクされていません",
|
||||
"account.modal.unlink.title": "アカウントのリンクを解除",
|
||||
"account.modal.unlink.description": "ソーシャルアカウントのリンクを解除すると、ユーザー名とパスワードを覚えていない場合にアカウントへのアクセスが失われる可能性があります。",
|
||||
"account.notify.oauth.unlinked.success": "リンク解除に成功しました",
|
||||
"account.card.security.title": "セキュリティ",
|
||||
"account.card.security.totp.enable.description": "2段階認証を有効にするため、現在のパスワードを入力してください",
|
||||
"account.card.security.totp.disable.description": "2段階認証を無効にするため、現在のパスワードを入力してください",
|
||||
"account.card.security.totp.button.start": "開始",
|
||||
"account.modal.totp.title": "2段階認証を有効にする",
|
||||
"account.modal.totp.step1": "ステップ1: 認証アプリを追加する",
|
||||
"account.modal.totp.step2": "ステップ2: コードを検証",
|
||||
"account.modal.totp.enterManually": "手動で入力",
|
||||
"account.modal.totp.code": "コピー",
|
||||
"account.modal.totp.clickToCopy": "ここをクリックしてコピー",
|
||||
"account.modal.totp.verify": "検証",
|
||||
"account.notify.totp.disable": "2段階認証の無効化に成功しました",
|
||||
"account.notify.totp.enable": "2段階認証の有効化に成功しました",
|
||||
"account.card.language.title": "言語",
|
||||
"account.card.language.description": "プロジェクトはコミュニティによって翻訳されています。一部の言語の翻訳は不完全の場合があります。",
|
||||
"account.card.color.title": "カラースキーム",
|
||||
// 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?",
|
||||
"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": "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.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": "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?",
|
||||
"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": "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.",
|
||||
"account.reverseShares.title": "ファイルリクエスト",
|
||||
"account.reverseShares.description": "ファイルリクエストは、外部のユーザーにファイルをアップロードしてもらえるユニークなURLを生成できます。",
|
||||
"account.reverseShares.title.empty": "まだ何もありません 👀",
|
||||
"account.reverseShares.description.empty": "ファイルリクエストがありません。",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"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.",
|
||||
"account.reverseShares.modal.title": "ファイルリクエストを作成",
|
||||
"account.reverseShares.modal.expiration.label": "有効期限",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "分間",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "分間",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "時間",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "時間",
|
||||
"account.reverseShares.modal.expiration.day-singular": "日間",
|
||||
"account.reverseShares.modal.expiration.day-plural": "日間",
|
||||
"account.reverseShares.modal.expiration.week-singular": "週間",
|
||||
"account.reverseShares.modal.expiration.week-plural": "週間",
|
||||
"account.reverseShares.modal.expiration.month-singular": "ヶ月",
|
||||
"account.reverseShares.modal.expiration.month-plural": "ヶ月",
|
||||
"account.reverseShares.modal.expiration.year-singular": "年間",
|
||||
"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": "このURLを使用してファイルをアップロードできる最大回数です。",
|
||||
"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": "Administration",
|
||||
"admin.button.users": "User management",
|
||||
"admin.button.config": "Configuration",
|
||||
"admin.version": "Version",
|
||||
"admin.title": "管理画面",
|
||||
"admin.button.users": "ユーザー管理",
|
||||
"admin.button.config": "設定",
|
||||
"admin.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?",
|
||||
"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": "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.",
|
||||
"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",
|
||||
"upload.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
|
||||
"upload.title": "アップロード",
|
||||
"upload.notify.generic-error": "共有を仕上げている最中にエラーが発生しました。",
|
||||
"upload.notify.count-failed": "{count} ファイルがアップロードに失敗しました。再度お試しください。",
|
||||
// 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}.",
|
||||
"upload.dropzone.title": "ファイルをアップロード",
|
||||
"upload.dropzone.description": "ここにファイルをドラッグしてアップロードを開始しましょう。{maxSize} 以下のファイルがアップロードできます。",
|
||||
"upload.dropzone.notify.file-too-big": "アップロードしようとしたファイルは、最大ファイルサイズの{maxSize} を超えています。",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Name",
|
||||
"upload.filelist.size": "Size",
|
||||
"upload.filelist.name": "ファイル名",
|
||||
"upload.filelist.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",
|
||||
"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.expires.error.too-long": "設定可能な有効期限を越えています。有効期限の上限は{max} です。",
|
||||
"upload.modal.link.label": "リンク",
|
||||
"upload.modal.expires.label": "有効期限",
|
||||
"upload.modal.expires.minute-singular": "分間",
|
||||
"upload.modal.expires.minute-plural": "分間",
|
||||
"upload.modal.expires.hour-singular": "時間",
|
||||
"upload.modal.expires.hour-plural": "時間",
|
||||
"upload.modal.expires.day-singular": "日間",
|
||||
"upload.modal.expires.day-plural": "日間",
|
||||
"upload.modal.expires.week-singular": "週間",
|
||||
"upload.modal.expires.week-plural": "週間",
|
||||
"upload.modal.expires.month-singular": "ヶ月間",
|
||||
"upload.modal.expires.month-plural": "ヶ月間",
|
||||
"upload.modal.expires.year-singular": "年間",
|
||||
"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": "This share will never expire.",
|
||||
"upload.modal.completed.expires-on": "This share will expire on {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Share ready",
|
||||
"upload.modal.completed.never-expires": "この共有は期限切れになりません。",
|
||||
"upload.modal.completed.expires-on": "この共有は、{expiration} に期限切れとなります。",
|
||||
"upload.modal.completed.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.",
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "編集 {shareId}",
|
||||
"share.edit.append-upload": "ファイルを追加",
|
||||
"share.edit.notify.generic-error": "共有の最終処理でエラーが発生しました。",
|
||||
"share.edit.notify.save-success": "共有の更新に成功しました",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuration",
|
||||
"admin.config.category.general": "General",
|
||||
"admin.config.category.share": "Share",
|
||||
"admin.config.category.email": "Email",
|
||||
"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 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",
|
||||
"admin.config.category.oauth": "ソーシャルログイン",
|
||||
"admin.config.general.app-name": "アプリ名",
|
||||
"admin.config.general.app-name.description": "アプリの名前",
|
||||
"admin.config.general.app-url": "アプリ名",
|
||||
"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": "ロゴ",
|
||||
"admin.config.general.logo.description": "新しい画像をアップロードしてロゴを変更できます。画像は、PNG形式でアスペクト比が1:1である必要があります。",
|
||||
"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} - 共有のURL\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} は、作成者の名前とURLにより置き換えられます。",
|
||||
"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} は、実際のパスワードリセットURLによって置き換えられます。",
|
||||
"admin.config.email.invite-subject": "無効な件名",
|
||||
"admin.config.email.invite-subject.description": "管理者がユーザーを招待したときに送信されるメールの件名です。",
|
||||
"admin.config.email.invite-message": "無効な本文",
|
||||
"admin.config.email.invite-message.description": "管理者がユーザーを招待したときに送信されるメールの本文です。{url} は、招待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-expiration": "有効期限の上限",
|
||||
"admin.config.share.max-expiration.description": "共有に設定可能な有効期限の上限を時間単位で設定できます。0を設定すると、有効期限が無制限になります。",
|
||||
"admin.config.share.max-size": "最大ファイルサイズ",
|
||||
"admin.config.share.max-size.description": "最大ファイルサイズ(byte単位)",
|
||||
"admin.config.share.zip-compression-level": "Zip圧縮レベル",
|
||||
"admin.config.share.zip-compression-level.description": "ファイルサイズと圧縮速度のバランスを取るように、レベルを調整できます。有効な値は0~9の間で、0が無圧縮、9で最大限の圧縮となります。 ",
|
||||
"admin.config.smtp.enabled": "有効",
|
||||
"admin.config.smtp.enabled.description": "SMTPを有効にするかどうかを選択してください。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": "テストメールを送信",
|
||||
"admin.config.oauth.allow-registration": "登録を許可する",
|
||||
"admin.config.oauth.allow-registration.description": "ユーザーにソーシャルアカウント経由での登録を許可します",
|
||||
"admin.config.oauth.ignore-totp": "二段階認証を無視する",
|
||||
"admin.config.oauth.ignore-totp.description": "ソーシャルログイン時に二段階認証を無視するかどうかを設定します",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "GitHubアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.github-client-id": "GitHub クライアントID",
|
||||
"admin.config.oauth.github-client-id.description": "GitHub OAuthアプリのクライアントID",
|
||||
"admin.config.oauth.github-client-secret": "GitHub クライアントシークレット",
|
||||
"admin.config.oauth.github-client-secret.description": "GitHub OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Googleアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.google-client-id": "Google クライアントID",
|
||||
"admin.config.oauth.google-client-id.description": "Google OAuthアプリのクライアントID",
|
||||
"admin.config.oauth.google-client-secret": "Google クライアントシークレット",
|
||||
"admin.config.oauth.google-client-secret.description": "Google OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Microsoftアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoftテナント",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Microsoft OAuthアプリのテナントID\ncommon: 個人のMicrosoftアカウントとMicrosoft Entra IDの職場または学校のアカウントを持つユーザーは、アプリケーションにサインインできます。 \norganizations: Microsoft Entra IDからの職場または学校のアカウントを持つユーザーのみがアプリケーションにサインインできます。\nconsumers: 個人のMicrosoftアカウントを持つユーザーのみがアプリケーションにサインインできます。\nMicrosoft Entraテナントのドメイン名またはGUID形式のテナントID: 特定のMicrosoft Entraテナント (職場または学校のアカウントを持つディレクトリメンバーまたはパーソナルMicrosoftアカウントを持つディレクトリゲスト) からのユーザーのみがアプリケーションにサインインできます。",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft クライアントID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Microsoft OAuthアプリのクライアントID",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft クライアントシークレット",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Microsoft OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Discordアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord クライアントID",
|
||||
"admin.config.oauth.discord-client-id.description": "Discord OAuthアプリのクライアントID",
|
||||
"admin.config.oauth.discord-client-secret": "Discord クライアントシークレット",
|
||||
"admin.config.oauth.discord-client-secret.description": "Discord OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Oops this page doesn't exist.",
|
||||
"404.button.home": "Bring me back home",
|
||||
"404.description": "ページが見つかりません。",
|
||||
"404.button.home": "ホームに戻る",
|
||||
// error
|
||||
"error.title": "エラー",
|
||||
"error.description": "申し訳ありません",
|
||||
"error.button.back": "戻る",
|
||||
"error.msg.default": "問題が発生しました。",
|
||||
"error.msg.access_denied": "認証処理を中止しました、後で再度お試しください。",
|
||||
"error.msg.expired_token": "認証処理に時間がかかりすぎています、後で再度お試しください。",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "この{0} アカウントにリンクしたユーザーが存在しません。",
|
||||
"error.msg.no_email": "この{0} アカウントからメールアドレスを取得出来ません。",
|
||||
"error.msg.already_linked": "この{0} アカウントは、既に別のアカウントにリンクされています。",
|
||||
"error.msg.not_linked": "この{0} アカウントはどのアカウントにもリンクされていません。",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// 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"
|
||||
"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": "成功",
|
||||
"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": "これは必須項目です"
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Verzenden",
|
||||
"navbar.upload": "Delen",
|
||||
"navbar.signin": "Aanmelden",
|
||||
"navbar.home": "Startpagina",
|
||||
"navbar.signup": "Registreren",
|
||||
"navbar.links.shares": "Mijn gedeelde bestanden",
|
||||
"navbar.links.reverse": "Reverse shares",
|
||||
"navbar.links.reverse": "Omgekeerd delen",
|
||||
"navbar.avatar.account": "Mijn account",
|
||||
"navbar.avatar.admin": "Beheer",
|
||||
"navbar.avatar.signout": "Afmelden",
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "Aanmelden",
|
||||
"signIn.notify.totp-required.title": "Tweestapsverificatie vereist",
|
||||
"signIn.notify.totp-required.description": "Voer uw tweestapsverificatiecode in",
|
||||
"signIn.oauth.or": "OF",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Account aanmaken",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Uw e-mailadres",
|
||||
"signup.button.submit": "Laten we beginnen",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authenticatie",
|
||||
"totp.button.signIn": "Aanmelden",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Wachtwoord vergeten?",
|
||||
"resetPassword.description": "Voer uw e-mailadres in om uw wachtwoord opnieuw in te stellen.",
|
||||
@@ -60,9 +70,22 @@ export default {
|
||||
"account.card.info.email": "E-mailadres",
|
||||
"account.notify.info.success": "Account succesvol bijgewerkt",
|
||||
"account.card.password.title": "Wachtwoord",
|
||||
"account.card.password.old": "Oud wachtwoord",
|
||||
"account.card.password.old": "Oude wachtwoord",
|
||||
"account.card.password.new": "Nieuw wachtwoord",
|
||||
"account.notify.password.success": "Wachtwoord succesvol gewijzigd",
|
||||
"account.card.password.noPasswordSet": "Je hebt nog geen wachtwoord ingesteld. Stel deze eerst in als je via email wilt inloggen.",
|
||||
"account.notify.password.success": "Wachtwoord succesvol gewijzigd!",
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Koppelen",
|
||||
"account.card.oauth.unlink": "Ontkoppel",
|
||||
"account.card.oauth.unlinked": "Niet-gekoppeld",
|
||||
"account.modal.unlink.title": "Ontkoppel account",
|
||||
"account.modal.unlink.description": "Het ontkoppelen van je sociale accounts kan leiden het verlies van je account. Vergeet je gebruikersnaam en wachtwoord niet!",
|
||||
"account.notify.oauth.unlinked.success": "Ontkoppelen succesvol",
|
||||
"account.card.security.title": "Beveiliging",
|
||||
"account.card.security.totp.enable.description": "Voer uw huidige wachtwoord in om TOTP in te schakelen",
|
||||
"account.card.security.totp.disable.description": "Voer uw huidige wachtwoord in om TOTP uit te schakelen",
|
||||
@@ -78,7 +101,7 @@ export default {
|
||||
"account.notify.totp.enable": "TOTP succesvol ingeschakeld",
|
||||
"account.card.language.title": "Taal",
|
||||
"account.card.language.description": "Het project is vertaald door de community. Sommige talen zijn mogelijk onvolledig.",
|
||||
"account.card.color.title": "Kleuren schema",
|
||||
"account.card.color.title": "Kleurenschema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Donker",
|
||||
"account.theme.light": "Licht",
|
||||
@@ -102,16 +125,16 @@ export default {
|
||||
"account.shares.table.size": "Grootte",
|
||||
"account.shares.modal.share-informations": "Gegevens delen",
|
||||
"account.shares.modal.share-link": "Deel link",
|
||||
"account.shares.modal.delete.title": "Delete share {share}",
|
||||
"account.shares.modal.delete.description": "Do you really want to delete this share?",
|
||||
"account.shares.modal.delete.title": "Verwijder share {share}",
|
||||
"account.shares.modal.delete.description": "Weet u zeker dat u deze share wilt verwijderen?",
|
||||
// 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": "Omgekeerde share",
|
||||
"account.reverseShares.description": "Met een omgekeerde share kan een externe gebruiker via unieke genereerde URL bestanden uploaden.",
|
||||
"account.reverseShares.title.empty": "Het is hier leeg 👀",
|
||||
"account.reverseShares.description.empty": "You don't have any reverse shares.",
|
||||
"account.reverseShares.description.empty": "Je heb nog geen omgekeerde shares, maak er een aan om ze hier te zien.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"account.reverseShares.modal.title": "Creeër een omgekeerde share",
|
||||
"account.reverseShares.modal.expiration.label": "Vervaldatum",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minuut",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minuten",
|
||||
@@ -125,26 +148,26 @@ export default {
|
||||
"account.reverseShares.modal.expiration.month-plural": "Maanden",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Jaar",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Jaren",
|
||||
"account.reverseShares.modal.max-size.label": "Max share size",
|
||||
"account.reverseShares.modal.max-size.label": "Max share grote",
|
||||
"account.reverseShares.modal.send-email": "Stuur e-mail notificatie",
|
||||
"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.modal.send-email.description": "Stuur een email notificatie wanneer er bestanden zijn gedeeld via deze omgekeerde share link.",
|
||||
"account.reverseShares.modal.max-use.label": "Max. gebruikte keren",
|
||||
"account.reverseShares.modal.max-use.description": "Maximale keren dat deze URL gebruikt kan worden om een share aan te maken.",
|
||||
"account.reverseShare.never-expires": "Deze omgekeerde share zal nooit verlopen.",
|
||||
"account.reverseShare.expires-on": "Dit omgekeerde share verloopt op {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Nog geen omgekeerde share aangemaakt",
|
||||
"account.reverseShares.table.count.singular": "deel",
|
||||
"account.reverseShares.table.count.plural": "delen",
|
||||
"account.reverseShares.table.shares": "Delen",
|
||||
"account.reverseShares.table.remaining": "Nog te gebruiken",
|
||||
"account.reverseShares.table.max-size": "Maximale share grote",
|
||||
"account.reverseShares.table.expires": "Verloopt op",
|
||||
"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.",
|
||||
"account.reverseShares.modal.reverse-share-link": "Omgekeerde share link",
|
||||
"account.reverseShares.modal.delete.title": "Omgekeerde share verwijderen",
|
||||
"account.reverseShares.modal.delete.description": "Weet u zeker dat u deze omgekeerde share wilt verwijderen? Als u dit doet, worden de bijbehorende bestanden ook verwijderd.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Beheer",
|
||||
"admin.title": "Instellingen",
|
||||
"admin.button.users": "Gebruikers beheer",
|
||||
"admin.button.config": "Configuratie",
|
||||
"admin.version": "Versie",
|
||||
@@ -161,7 +184,7 @@ export default {
|
||||
"admin.users.edit.update.change-password.button": "Nieuw wachtwoord opslaan",
|
||||
"admin.users.edit.update.notify.password.success": "Wachtwoord succesvol gewijzigd",
|
||||
"admin.users.edit.delete.title": "Gebruiker {username} verwijderen",
|
||||
"admin.users.edit.delete.description": "Do you really want to delete this user and all his shares?",
|
||||
"admin.users.edit.delete.description": "Wilt u echt deze gebruiker en al zijn shares verwijderen?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Gebruiker aanmaken",
|
||||
"admin.users.modal.create.username": "Gebruikersnaam",
|
||||
@@ -174,23 +197,24 @@ export default {
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Uploaden",
|
||||
"upload.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"upload.notify.count-failed": "{count} bestanden konden niet worden geüpload. Opnieuw proberen.",
|
||||
"upload.notify.generic-error": "Er is een fout opgetreden tijdens het voltooien van uw share.",
|
||||
"upload.notify.count-failed": "{count} bestanden konden niet worden geüpload. nogmaals opnieuw proberen.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Bestanden uploaden",
|
||||
"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}.",
|
||||
"upload.dropzone.description": "Sleep je bestanden hier om je share te beginnen. We kunnen alleen bestanden accepteren die kleiner zijn dan {maxSize} in totaal.",
|
||||
"upload.dropzone.notify.file-too-big": "Uw bestanden overschrijden de maximale share grootte van {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Naam",
|
||||
"upload.filelist.size": "Grootte",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Create Share",
|
||||
"upload.modal.link.error.invalid": "Can only contain letters, numbers, underscores, and hyphens",
|
||||
"upload.modal.title": "Share aanmaken",
|
||||
"upload.modal.link.error.invalid": "Het mag alleen letters, cijfers, onderstrepingstekens en koppeltekens bevatten",
|
||||
"upload.modal.link.error.taken": "Deze link is al in gebruik",
|
||||
"upload.modal.not-signed-in": "U bent niet aangemeld",
|
||||
"upload.modal.not-signed-in-description": "You will be unable to delete your share manually and view the visitor count.",
|
||||
"upload.modal.not-signed-in-description": "Het is niet mogelijk uw gedeelde bestanden handmatig te verwijderen en het aantal bezoekers te bekijken.",
|
||||
"upload.modal.expires.never": "nooit",
|
||||
"upload.modal.expires.never-long": "Verloopt nooit",
|
||||
"upload.modal.expires.error.too-long": "De maximale vervaldatum van {max} is overschreven.",
|
||||
"upload.modal.link.label": "Koppeling",
|
||||
"upload.modal.expires.label": "Vervaldatum",
|
||||
"upload.modal.expires.minute-singular": "Minuut",
|
||||
@@ -206,8 +230,8 @@ export default {
|
||||
"upload.modal.expires.year-singular": "Jaar",
|
||||
"upload.modal.expires.year-plural": "Jaren",
|
||||
"upload.modal.accordion.description.title": "Beschrijving",
|
||||
"upload.modal.accordion.description.placeholder": "Note for the recipients of this share",
|
||||
"upload.modal.accordion.email.title": "E-mail ontvangers",
|
||||
"upload.modal.accordion.description.placeholder": "Opmerking voor de ontvangers van deze share",
|
||||
"upload.modal.accordion.email.title": "E-mail van de ontvangers",
|
||||
"upload.modal.accordion.email.placeholder": "Voer e-mail ontvangers in",
|
||||
"upload.modal.accordion.email.invalid-email": "Ongeldig e-mailadres",
|
||||
"upload.modal.accordion.security.title": "Beveiliging opties",
|
||||
@@ -216,58 +240,65 @@ export default {
|
||||
"upload.modal.accordion.security.max-views.label": "Maximum aantal weergaven",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Onbeperkt",
|
||||
// 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",
|
||||
"upload.modal.completed.never-expires": "Deze omgekeerde share zal nooit verlopen.",
|
||||
"upload.modal.completed.expires-on": "Deze omgekeerde share verloopt op {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Share is gereed",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Share {shareId}",
|
||||
"share.description": "Kijk eens wat ik met je heb gedeeld!",
|
||||
"share.error.visitor-limit-exceeded.title": "Bezoekerslimiet overschreden",
|
||||
"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.error.visitor-limit-exceeded.description": "De bezoekerslimiet van deze share is overschreden.",
|
||||
"share.error.removed.title": "Share was verwijderd",
|
||||
"share.error.not-found.title": "Share niet gevonden",
|
||||
"share.error.not-found.description": "De share die je zoekt kan niet gevonden worden.",
|
||||
"share.modal.password.title": "Wachtwoord vereist",
|
||||
"share.modal.password.description": "To access this share please enter the password for the share.",
|
||||
"share.modal.password.description": "Vul een wachtwoord in om toegang te krijgen tot deze share.",
|
||||
"share.modal.password": "Wachtwoord",
|
||||
"share.modal.error.invalid-password": "Ongeldig wachtwoord",
|
||||
"share.button.download-all": "Alles downloaden",
|
||||
"share.notify.download-all-preparing": "The share is preparing. Try again in a few minutes.",
|
||||
"share.modal.file-link": "Bestand koppeling",
|
||||
"share.notify.download-all-preparing": "De share word gebundeld. Probeer het over een paar minuten opnieuw.",
|
||||
"share.modal.file-link": "Link naar bestand",
|
||||
"share.table.name": "Naam",
|
||||
"share.table.size": "Grootte",
|
||||
"share.modal.file-preview.error.not-supported.title": "Voorbeeld niet ondersteund",
|
||||
"share.modal.file-preview.error.not-supported.description": "Een voorbeeld voor dit bestandstype wordt niet ondersteund. Download het bestand om het te bekijken.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "{shareId} bewerken",
|
||||
"share.edit.append-upload": "Bestand toevoegen",
|
||||
"share.edit.notify.generic-error": "Er is een fout opgetreden tijdens het voltooien van uw share.",
|
||||
"share.edit.notify.save-success": "Share is succesvol bijgewerkt",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Configuratie",
|
||||
"admin.config.category.general": "Algemeen",
|
||||
"admin.config.category.share": "Delen",
|
||||
"admin.config.category.email": "E-mail",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Social login",
|
||||
"admin.config.general.app-name": "App naam",
|
||||
"admin.config.general.app-name.description": "Naam van de applicatie",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"admin.config.general.app-url.description": "On which URL Pingvin Share is available",
|
||||
"admin.config.general.app-url.description": "De URL waar Pingvin Delen bereikbaar is",
|
||||
"admin.config.general.show-home-page": "Toon startpagina",
|
||||
"admin.config.general.show-home-page.description": "Toon of verberg de home pagina",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Verander uw logo door een nieuwe afbeelding te uploaden. De afbeelding moet PNG zijn en het formaat moet 1:1 hebben.",
|
||||
"admin.config.general.logo.placeholder": "Afbeelding kiezen",
|
||||
"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 wachtwoord onderwerp",
|
||||
"admin.config.email.reset-password-subject.description": "Onderwerp van de e-mail die wordt verzonden wanneer een gebruiker een wachtwoordreset aanvraagt.",
|
||||
"admin.config.email.reset-password-message": "Wachtwoord opnieuw instellen bericht",
|
||||
"admin.config.email.enable-share-email-recipients": "Voer e-mail ontvangers in",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Sta e-mails toe om de ontvangers de deel link te versturen. Schakel dit alleen in als je SMTP hebt ingeschakeld.",
|
||||
"admin.config.email.share-recipients-subject": "Share Email onderwerp",
|
||||
"admin.config.email.share-recipients-subject.description": "Het onderwerp van de e-mail die wordt verzonden naar de share ontvangers.",
|
||||
"admin.config.email.share-recipients-message": "Bericht voor ontvangers van de Share",
|
||||
"admin.config.email.share-recipients-message.description": "Bericht dat wordt verzonden naar de share ontvangers. Beschikbare variabelen:\n {creator} - De gebruikersnaam van de maker van het aandeel\n {shareUrl} - De URL van de share\n {desc} - De beschrijving van aandeel\n {expires} - De vervaldatum van het aandeel\n De variabelen worden vervangen door hun werkelijke waarde bij verzending.",
|
||||
"admin.config.email.reverse-share-subject": "Omgekeerde share email onderwerp",
|
||||
"admin.config.email.reverse-share-subject.description": "Het onderwerp van de e-mail die wordt verzonden wanneer iemand een share heeft gemaakt met uw omgekeerde share link.",
|
||||
"admin.config.email.reverse-share-message": "Omgekeerde share bericht",
|
||||
"admin.config.email.reverse-share-message.description": "Het bericht dat wordt verzonden wanneer iemand een share heeft aangemaakt via de omgekeerde share link. {shareUrl} zal worden vervangen door de naam van de maker en de URL van de share.",
|
||||
"admin.config.email.reset-password-subject": "Reset wachtwoord email onderwerp",
|
||||
"admin.config.email.reset-password-subject.description": "Het onderwerp van de e-mail die wordt verzonden wanneer een gebruiker een wachtwoord reset aanvraagt.",
|
||||
"admin.config.email.reset-password-message": "Wachtwoord opnieuw aanvraag bericht",
|
||||
"admin.config.email.reset-password-message.description": "Bericht dat wordt verzonden wanneer een gebruiker een wachtwoord reset aanvraagt. {url} zal worden vervangen door de wachtwoord reset URL.",
|
||||
"admin.config.email.invite-subject": "Onderwerp uitnodiging",
|
||||
"admin.config.email.invite-subject.description": "Onderwerp van de e-mail die wordt verzonden wanneer een beheerder een gebruiker uitnodigt.",
|
||||
@@ -275,10 +306,12 @@ export default {
|
||||
"admin.config.email.invite-message.description": "Bericht dat wordt verzonden wanneer een beheerder een gebruiker uitnodigt. {url} zal worden vervangen door de uitnodigings-URL en {password} met het wachtwoord.",
|
||||
"admin.config.share.allow-registration": "Sta registratie toe",
|
||||
"admin.config.share.allow-registration.description": "Of registratie is toegestaan",
|
||||
"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 grootte",
|
||||
"admin.config.share.max-size.description": "Maximum share size in bytes",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Ongeverifieerde shares toestaan",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Laat niet-geverifieerde gebruikers toe om shares aan te kunnen aanmaken",
|
||||
"admin.config.share.max-expiration": "Max. vervaldatum",
|
||||
"admin.config.share.max-expiration.description": "Maximale verval tijd van de share in uren. Stel in op 0 om onbeperkte vervaldatum toe te staan.",
|
||||
"admin.config.share.max-size": "Max. grootte",
|
||||
"admin.config.share.max-size.description": "Maximale share grootte in bytes",
|
||||
"admin.config.share.zip-compression-level": "Zip compressie niveau",
|
||||
"admin.config.share.zip-compression-level.description": "Pas het niveau aan voor evenwicht tussen bestandsgrootte en compressie snelheid. Geldige waarden variëren van 0 tot 9, waarbij 0 geen compressie is en 9 de maximale compressie is. ",
|
||||
"admin.config.smtp.enabled": "Inschakelen",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Wachtwoord",
|
||||
"admin.config.smtp.password.description": "Wachtwoord van de SMTP-server",
|
||||
"admin.config.smtp.button.test": "Test e-mail verzenden",
|
||||
"admin.config.oauth.allow-registration": "Sta registratie toe",
|
||||
"admin.config.oauth.allow-registration.description": "Gebruikers toestaan zich te registreren via sociale login",
|
||||
"admin.config.oauth.ignore-totp": "TOTP negeren",
|
||||
"admin.config.oauth.ignore-totp.description": "TOTP negeren wanneer gebruiker sociale login gebruikt",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Ofdat GitHub login is ingeschakeld",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client-ID van de GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret van de GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Ofdat GitHub login is ingeschakeld",
|
||||
"admin.config.oauth.google-client-id": "Google Client-ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client-ID van de Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret van de Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Ofdat Microsoft login is ingeschakeld",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID van de Microsoft OAuth app\ncommon: Gebruikers met zowel een persoonlijk Microsoft-account als een werk- of schoolaccount van Microsoft Entra ID kunnen zich aanmelden bij de applicatie. organisaties: alleen gebruikers met werk of schoolaccounts van Microsoft Entra ID kunnen zich aanmelden bij de applicatie.\nconsumenten: Alleen gebruikers met een persoonlijk Microsoft-account kunnen zich aanmelden bij de applicatie.\ndomeinnaam van de Microsoft Entra tenant of de tenant ID in GUID formaat: alleen gebruikers van een specifieke Microsoft Entra tenant (directory leden met een werk- of schoolaccount of map gasten met een persoonlijk Microsoft-account) kunnen zich aanmelden bij de toepassing.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client-ID van de Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client-secret van de Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Ofdat discord login is ingeschakeld",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord beperkte server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Beperk het inloggen tot gebruikers in een specifieke server. Laat leeg om uit te schakelen.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client-ID van de discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client-secret van de discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Ofdat OpenID Connect login is ingeschakeld",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI van de OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Gebruikersnaam claim in OpenID Connect-ID-token. Laat het leeg als u niet weet wat deze configuratie is.",
|
||||
"admin.config.oauth.oidc-client-id": "Client-ID OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client-ID van de OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret van de OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Oeps, deze pagina bestaat niet.",
|
||||
"404.button.home": "Breng me terug naar huis",
|
||||
// error
|
||||
"error.title": "Fout",
|
||||
"error.description": "Oeps!",
|
||||
"error.button.back": "Terug",
|
||||
"error.msg.default": "Er ging iets mis.",
|
||||
"error.msg.access_denied": "U heeft het verificatieproces geannuleerd, probeer het opnieuw.",
|
||||
"error.msg.expired_token": "Het verificatieproces duurde te lang. Probeer het opnieuw.",
|
||||
"error.msg.invalid_token": "Interne fout",
|
||||
"error.msg.no_user": "De gekoppelde gebruiker aan {0} account bestaat niet.",
|
||||
"error.msg.no_email": "Kan geen e-mailadres ophalen van dit {0} account.",
|
||||
"error.msg.already_linked": "Dit {0} account is al gekoppeld aan een ander account.",
|
||||
"error.msg.not_linked": "Dit {0} account is nog aan geen enkel account gekoppeld.",
|
||||
"error.msg.unverified_account": "Dit {0} account is nog niet geverifieerd, probeer het opnieuw na de verificatie.",
|
||||
"error.msg.discord_guild_permission_denied": "U heeft geen toestemming om in te loggen.",
|
||||
"error.msg.cannot_get_user_info": "Kan uw gebruikersgegevens van dit {0} account niet ophalen.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Opslaan",
|
||||
"common.button.create": "Aanmaken",
|
||||
|
||||
419
frontend/src/i18n/translations/pl-PL.ts
Normal file
419
frontend/src/i18n/translations/pl-PL.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Prześlij",
|
||||
"navbar.signin": "Zaloguj się",
|
||||
"navbar.home": "Strona główna",
|
||||
"navbar.signup": "Zarejestruj się",
|
||||
"navbar.links.shares": "Moje udziały",
|
||||
"navbar.links.reverse": "Udziały odwrotne",
|
||||
"navbar.avatar.account": "Moje konto",
|
||||
"navbar.avatar.admin": "Administracja",
|
||||
"navbar.avatar.signout": "Wyloguj się",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "<h>Samodzielnie utrzymywana</h> platforma udostępniania plików.",
|
||||
"home.description": "Czy na pewno chcesz przekazać swoje prywatne pliki w ręce osób trzecich, takich jak WeTransfer?",
|
||||
"home.bullet.a.name": "Samodzielnie Utrzymywana",
|
||||
"home.bullet.a.description": "Utrzymuj Pingvin Share na własnym serwerze.",
|
||||
"home.bullet.b.name": "Prywatność",
|
||||
"home.bullet.b.description": "To są twoje pliki i nigdy nie powinny trafiać w ręce osób trzecich.",
|
||||
"home.bullet.c.name": "Brak irytującego ograniczenia rozmiaru pliku",
|
||||
"home.bullet.c.description": "Prześlij tak duże pliki, jak chcesz. Ograniczeniem będzie tylko twój dysk twardy.",
|
||||
"home.button.start": "Zaczynajmy",
|
||||
"home.button.source": "Kod źródłowy",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Witaj ponownie",
|
||||
"signin.description": "Nie masz jeszcze konta?",
|
||||
"signin.button.signup": "Zarejestruj się",
|
||||
"signin.input.email-or-username": "Adres e-mail lub nazwa użytkownika",
|
||||
"signin.input.email-or-username.placeholder": "Adres e-mail lub nazwa użytkownika",
|
||||
"signin.input.password": "Hasło",
|
||||
"signin.input.password.placeholder": "Twoje hasło",
|
||||
"signin.button.submit": "Zaloguj się",
|
||||
"signIn.notify.totp-required.title": "Wymagane jest uwierzytelnianie dwuetapowe",
|
||||
"signIn.notify.totp-required.description": "Wprowadź kod uwierzytelniania dwuetapowego",
|
||||
"signIn.oauth.or": "LUB",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Utwórz konto",
|
||||
"signup.description": "Masz już konto?",
|
||||
"signup.button.signin": "Zaloguj się",
|
||||
"signup.input.username": "Nazwa użytkownika",
|
||||
"signup.input.username.placeholder": "Twoja nazwa użytkownika",
|
||||
"signup.input.email": "Adres e-mail",
|
||||
"signup.input.email.placeholder": "Twój adres e-mail",
|
||||
"signup.button.submit": "Zaczynajmy",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Uwierzytelnianie TOTP",
|
||||
"totp.button.signIn": "Zaloguj się",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Nie pamiętasz hasła?",
|
||||
"resetPassword.description": "Wprowadź swój e-mail, aby zresetować swoje hasło.",
|
||||
"resetPassword.notify.success": "Został wysłany e-mail z linkiem do resetowania hasła.",
|
||||
"resetPassword.button.back": "Powrót do strony logowania",
|
||||
"resetPassword.text.resetPassword": "Resetuj hasło",
|
||||
"resetPassword.text.enterNewPassword": "Wprowadź nowe hasło",
|
||||
"resetPassword.input.password": "Nowe hasło",
|
||||
"resetPassword.notify.passwordReset": "Hasło zostało pomyślnie zresetowane.",
|
||||
// /account
|
||||
"account.title": "Moje konto",
|
||||
"account.card.info.title": "Informacje o koncie",
|
||||
"account.card.info.username": "Nazwa użytkownika",
|
||||
"account.card.info.email": "Adres e-mail",
|
||||
"account.notify.info.success": "Konto zostało zaktualizowane",
|
||||
"account.card.password.title": "Hasło",
|
||||
"account.card.password.old": "Dotychczasowe hasło",
|
||||
"account.card.password.new": "Nowe hasło",
|
||||
"account.card.password.noPasswordSet": "Nie masz ustawionego hasła. Jeśli chcesz zalogować się za pomocą adresu e-mail i hasła, musisz ustawić hasło.",
|
||||
"account.notify.password.success": "Hasło zostało pomyślnie zmienione",
|
||||
"account.card.oauth.title": "Logowanie za pomocą konta społecznościowego",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Połącz",
|
||||
"account.card.oauth.unlink": "Odłącz",
|
||||
"account.card.oauth.unlinked": "Rozłączono",
|
||||
"account.modal.unlink.title": "Odłącz konto",
|
||||
"account.modal.unlink.description": "Odłączenie kont społecznościowych, jeśli nie pamiętasz nazwy użytkownika i hasła, może spowodować utratę konta.",
|
||||
"account.notify.oauth.unlinked.success": "Odłączono pomyślnie",
|
||||
"account.card.security.title": "Zabezpieczenia",
|
||||
"account.card.security.totp.enable.description": "Wprowadź aktualne hasło, aby móc włączyć TOTP",
|
||||
"account.card.security.totp.disable.description": "Wprowadź aktualne hasło, aby wyłączyć TOTP",
|
||||
"account.card.security.totp.button.start": "Uruchom",
|
||||
"account.modal.totp.title": "Uruchom TOTP",
|
||||
"account.modal.totp.step1": "Krok 1: Dodaj swój uwierzytelniacz",
|
||||
"account.modal.totp.step2": "Krok 2: Potwierdź swój kod",
|
||||
"account.modal.totp.enterManually": "Wpisz ręcznie",
|
||||
"account.modal.totp.code": "Kod",
|
||||
"account.modal.totp.clickToCopy": "Kliknij, aby skopiować",
|
||||
"account.modal.totp.verify": "Weryfikuj",
|
||||
"account.notify.totp.disable": "%s wyłączono pomyślnie",
|
||||
"account.notify.totp.enable": "TOTP włączono pomyślnie",
|
||||
"account.card.language.title": "Język",
|
||||
"account.card.language.description": "Projekt jest tłumaczony przez społeczność. Niektóre języki mogą być niekompletne.",
|
||||
"account.card.color.title": "Schemat kolorów",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Ciemny",
|
||||
"account.theme.light": "Jasny",
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Usuń Konto",
|
||||
"account.modal.delete.title": "Usuń Konto",
|
||||
"account.modal.delete.description": "Czy na pewno chcesz usunąć swoje konto, w tym wszystkie aktywne udziały?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Moje udziały",
|
||||
"account.shares.title.empty": "Tu jest pusto 👀",
|
||||
"account.shares.description.empty": "Nie masz żadnych udziałów.",
|
||||
"account.shares.button.create": "Utwórz go",
|
||||
"account.shares.info.title": "Informacje udziału",
|
||||
"account.shares.table.id": "Nr identyfikacyjny",
|
||||
"account.shares.table.name": "Nazwa",
|
||||
"account.shares.table.description": "Opis",
|
||||
"account.shares.table.visitors": "Odwiedzający",
|
||||
"account.shares.table.expiresAt": "Wygasa",
|
||||
"account.shares.table.createdAt": "Utworzono",
|
||||
"account.shares.table.size": "Rozmiar",
|
||||
"account.shares.modal.share-informations": "Informacje udziału",
|
||||
"account.shares.modal.share-link": "Udostępnij link",
|
||||
"account.shares.modal.delete.title": "Usuń udostępnianie {share}",
|
||||
"account.shares.modal.delete.description": "Czy na pewno chcesz usunąć to udostępnianie?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Udziały odwrotne",
|
||||
"account.reverseShares.description": "Udostępnianie odwrotne pozwala na wygenerowanie unikalnego adresu URL, który umożliwia użytkownikom zewnętrznym na tworzenie udostępniania.",
|
||||
"account.reverseShares.title.empty": "Tu jest pusto 👀",
|
||||
"account.reverseShares.description.empty": "Nie masz żadnych udostępnień odwrotnych.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Utwórz udostępnianie odwrotne",
|
||||
"account.reverseShares.modal.expiration.label": "Wygasanie",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minuta",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minuty/t",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Godzina",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Godziny",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Dzień",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Dni",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Tydzień",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Tygodni/e",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Miesiąc",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Miesiące/ęcy",
|
||||
"account.reverseShares.modal.expiration.year-singular": "Rok",
|
||||
"account.reverseShares.modal.expiration.year-plural": "Lata/lat",
|
||||
"account.reverseShares.modal.max-size.label": "Maksymalny rozmiar udziału",
|
||||
"account.reverseShares.modal.send-email": "Wysyłanie powiadomienia e-mail",
|
||||
"account.reverseShares.modal.send-email.description": "Wyślij powiadomienie e-mail, gdy udostępnianie zostanie utworzone za pomocą linku udostępniania odwrotnego.",
|
||||
"account.reverseShares.modal.max-use.label": "Limit użyć",
|
||||
"account.reverseShares.modal.max-use.description": "Maksymalna ilość razy, kiedy ten adres URL może być użyty do utworzenia udostępniania.",
|
||||
"account.reverseShare.never-expires": "To udostępnienie odwrotne nigdy nie wygasa.",
|
||||
"account.reverseShare.expires-on": "To udostępnienie odwrotne wygaśnie dnia {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Nie utworzono jeszcze udostępnień",
|
||||
"account.reverseShares.table.count.singular": "udostępnij",
|
||||
"account.reverseShares.table.count.plural": "udziały",
|
||||
"account.reverseShares.table.shares": "Udziały",
|
||||
"account.reverseShares.table.remaining": "Pozostało miejsca",
|
||||
"account.reverseShares.table.max-size": "Maksymalny rozmiar udziału",
|
||||
"account.reverseShares.table.expires": "Wygasa",
|
||||
"account.reverseShares.modal.reverse-share-link": "Link udostępnienia odwrotnego",
|
||||
"account.reverseShares.modal.delete.title": "Usuń udostępnienie odwrotnie",
|
||||
"account.reverseShares.modal.delete.description": "Czy na pewno chcesz usunąć to udostępnienie odwrotne? Jeśli tak, to również zostaną usunięte powiązane z nim udostępnienia.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administracja",
|
||||
"admin.button.users": "Zarządzanie użytkownikami",
|
||||
"admin.button.config": "Konfiguracja",
|
||||
"admin.version": "Wersja",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Zarządzanie użytkownikami",
|
||||
"admin.users.table.username": "Nazwa użytkownika",
|
||||
"admin.users.table.email": "Adres e-mail",
|
||||
"admin.users.table.admin": "Administrator",
|
||||
"admin.users.edit.update.title": "Zaktualizuj użytkownika {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Uprawnienia administratora",
|
||||
"admin.users.edit.update.change-password.title": "Zmień hasło",
|
||||
"admin.users.edit.update.change-password.field": "Nowe hasło",
|
||||
"admin.users.edit.update.change-password.button": "Zapisz nowe hasło",
|
||||
"admin.users.edit.update.notify.password.success": "Hasło pomyślnie zmienione",
|
||||
"admin.users.edit.delete.title": "Usuń użytkownika {username}",
|
||||
"admin.users.edit.delete.description": "Czy na pewno chcesz usunąć tego użytkownika i wszystkie jego udziały?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Utwórz użytkownika",
|
||||
"admin.users.modal.create.username": "Nazwa użytkownika",
|
||||
"admin.users.modal.create.email": "Adres e-mail",
|
||||
"admin.users.modal.create.password": "Hasło",
|
||||
"admin.users.modal.create.manual-password": "Ustaw ręcznie hasło",
|
||||
"admin.users.modal.create.manual-password.description": "Jeśli nie zaznaczone, użytkownik otrzyma e-mail z linkiem do ustawienia hasła.",
|
||||
"admin.users.modal.create.admin": "Uprawnienia administratora",
|
||||
"admin.users.modal.create.admin.description": "Jeśli zaznaczone, użytkownik będzie miał dostęp do panelu administratora.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Prześlij",
|
||||
"upload.notify.generic-error": "Wystąpił błąd podczas zakończenia tworzenia udziału.",
|
||||
"upload.notify.count-failed": "Nie udało się przesłać {count} plików. Próbuję ponownie.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Prześlij pliki",
|
||||
"upload.dropzone.description": "Przeciągnij i upuść pliki tutaj, aby rozpocząć udostępnianie. Możemy zaakceptować tylko pliki mniejsze niż {maxSize}.",
|
||||
"upload.dropzone.notify.file-too-big": "Twoje pliki przekraczają maksymalny rozmiar {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Nazwa",
|
||||
"upload.filelist.size": "Rozmiar",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Utwórz Udział",
|
||||
"upload.modal.link.error.invalid": "Może zawierać tylko litery, cyfry, znaki podkreślenia i myślniki",
|
||||
"upload.modal.link.error.taken": "Ten link jest już w użyciu",
|
||||
"upload.modal.not-signed-in": "Nie jesteś zalogowany/a",
|
||||
"upload.modal.not-signed-in-description": "Nie możesz ręcznie usunąć swojego udziału ani wyświetlić licznika odwiedzających.",
|
||||
"upload.modal.expires.never": "nigdy",
|
||||
"upload.modal.expires.never-long": "Nigdy nie wygasa",
|
||||
"upload.modal.expires.error.too-long": "Termin ważności przekracza maksymalną datę wygaśnięcia {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Wygasanie",
|
||||
"upload.modal.expires.minute-singular": "Minuta",
|
||||
"upload.modal.expires.minute-plural": "Minuty/t",
|
||||
"upload.modal.expires.hour-singular": "Godzina",
|
||||
"upload.modal.expires.hour-plural": "Godzin/y",
|
||||
"upload.modal.expires.day-singular": "Dzień",
|
||||
"upload.modal.expires.day-plural": "Dni",
|
||||
"upload.modal.expires.week-singular": "Tydzień",
|
||||
"upload.modal.expires.week-plural": "Tygodni/e",
|
||||
"upload.modal.expires.month-singular": "Miesiąc",
|
||||
"upload.modal.expires.month-plural": "Miesiące/ęcy",
|
||||
"upload.modal.expires.year-singular": "Rok",
|
||||
"upload.modal.expires.year-plural": "Lat/a",
|
||||
"upload.modal.accordion.description.title": "Opis",
|
||||
"upload.modal.accordion.description.placeholder": "Notatka dla odbiorców tego udziału",
|
||||
"upload.modal.accordion.email.title": "Odbiorcy wiadomości e-mail",
|
||||
"upload.modal.accordion.email.placeholder": "Wprowadź adresatów wiadomości e-mail",
|
||||
"upload.modal.accordion.email.invalid-email": "Adres e-mail jest nieprawidłowy",
|
||||
"upload.modal.accordion.security.title": "Ustawienia bezpieczeństwa",
|
||||
"upload.modal.accordion.security.password.label": "Ochrona hasłem",
|
||||
"upload.modal.accordion.security.password.placeholder": "Brak hasła",
|
||||
"upload.modal.accordion.security.max-views.label": "Maksymalna liczba wyświetleń",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Bez ograniczeń",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "To udostępnienie nigdy nie wygaśnie.",
|
||||
"upload.modal.completed.expires-on": "To udostępnienie wygaśnie dnia {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Udostępnianie gotowe",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Udostępnij {shareId}",
|
||||
"share.description": "Spójrz, co ci udostępniłem!",
|
||||
"share.error.visitor-limit-exceeded.title": "Przekroczono limit odwiedzających",
|
||||
"share.error.visitor-limit-exceeded.description": "Limit odwiedzających dla tego udziału został przekroczony.",
|
||||
"share.error.removed.title": "Udostępnianie usunięte",
|
||||
"share.error.not-found.title": "Nie znaleziono udziału",
|
||||
"share.error.not-found.description": "Udział, który szukasz, nie istnieje.",
|
||||
"share.modal.password.title": "Wymagane hasło",
|
||||
"share.modal.password.description": "Aby uzyskać dostęp do tego udziału, wprowadź hasło.",
|
||||
"share.modal.password": "Hasło",
|
||||
"share.modal.error.invalid-password": "Hasło jest nieprawidłowe",
|
||||
"share.button.download-all": "Pobierz wszystko",
|
||||
"share.notify.download-all-preparing": "Udostępnianie jest przygotowywane. Spróbuj ponownie za kilka minut.",
|
||||
"share.modal.file-link": "Link do pliku",
|
||||
"share.table.name": "Nazwa",
|
||||
"share.table.size": "Rozmiar",
|
||||
"share.modal.file-preview.error.not-supported.title": "Podgląd nie jest obsługiwany",
|
||||
"share.modal.file-preview.error.not-supported.description": "Podgląd dla tego typu pliku nie jest obsługiwany. Pobierz plik, aby go zobaczyć.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edytuj {shareId}",
|
||||
"share.edit.append-upload": "Dołącz plik",
|
||||
"share.edit.notify.generic-error": "W trakcie zakańczania tworzenia udziału wystąpił błąd.",
|
||||
"share.edit.notify.save-success": "Udział zaktualizowany pomyślnie",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Konfiguracja",
|
||||
"admin.config.category.general": "Ogólne",
|
||||
"admin.config.category.share": "Udostępnij",
|
||||
"admin.config.category.email": "Adres e-mail",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Logowanie za pomocą konta społecznościowego",
|
||||
"admin.config.general.app-name": "Nazwa aplikacji",
|
||||
"admin.config.general.app-name.description": "Nazwa aplikacji",
|
||||
"admin.config.general.app-url": "Adres URL aplikacji",
|
||||
"admin.config.general.app-url.description": "Pod którym adresem URL Pingvin Share jest dostępny",
|
||||
"admin.config.general.show-home-page": "Pokaż stronę główną",
|
||||
"admin.config.general.show-home-page.description": "Czy wyświetlać stronę główną",
|
||||
"admin.config.general.logo": "Logo",
|
||||
"admin.config.general.logo.description": "Zmień logo, przesyłając nowy obraz. Obraz musi być plikiem PNG i mieć proporcje 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Wybierz obraz",
|
||||
"admin.config.email.enable-share-email-recipients": "Włącz adresatów wiadomości e-mail dla udziału",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Czy zezwolić na wysyłanie e-maili do adresatów udziału? Włącz to tylko wtedy, gdy włączyłeś SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Temat dla odbiorców udziału",
|
||||
"admin.config.email.share-recipients-subject.description": "Temat wiadomości e-mail, która zostanie wysłana do odbiorców udziału.",
|
||||
"admin.config.email.share-recipients-message": "Wiadomość dla odbiorców udziału",
|
||||
"admin.config.email.share-recipients-message.description": "Wiadomość, która zostanie wysłana do odbiorców udziału. Dostępne zmienne:\n {creator} - Nazwa użytkownika twórcy udziału\n {shareUrl} - Adres URL udziału\n {desc} - Opis udziału\n {expires} - Data wygaśnięcia udziału\n Zmienne zostaną zastąpione rzeczywistą wartością.",
|
||||
"admin.config.email.reverse-share-subject": "Temat udziału odwrotnego",
|
||||
"admin.config.email.reverse-share-subject.description": "Temat wiadomości e-mail, która zostanie wysłana, gdy ktoś utworzy udostępnienie za pomocą linku udziału odwrotnego.",
|
||||
"admin.config.email.reverse-share-message": "Wiadomość udziału odwrotnego",
|
||||
"admin.config.email.reverse-share-message.description": "Wiadomość, która zostanie wysłana, gdy ktoś utworzy udział za pomocą twojego linku udziału odwrotnego. {shareUrl} zostanie zastąpiony nazwą twórcy i adresem URL udziału.",
|
||||
"admin.config.email.reset-password-subject": "Temat resetowania hasła",
|
||||
"admin.config.email.reset-password-subject.description": "Temat wiadomości e-mail, która zostanie wysłana, gdy użytkownik zażąda zresetowania hasła.",
|
||||
"admin.config.email.reset-password-message": "Wiadomość resetowania hasła",
|
||||
"admin.config.email.reset-password-message.description": "Wiadomość, która zostanie wysłana, gdy użytkownik zażąda zresetowania hasła. {url} zostanie zastąpiony adresem URL resetowania hasła.",
|
||||
"admin.config.email.invite-subject": "Temat zaproszenia",
|
||||
"admin.config.email.invite-subject.description": "Temat wiadomości e-mail, która zostanie wysłana, gdy administrator zaprasza użytkownika.",
|
||||
"admin.config.email.invite-message": "Wiadomość zaproszenia",
|
||||
"admin.config.email.invite-message.description": "Wiadomość, która zostanie wysłana, gdy administrator zaprasza użytkownika. {url} zostanie zastąpiony adresem URL zaproszenia, a {password} hasłem.",
|
||||
"admin.config.share.allow-registration": "Zezwól na rejestrację",
|
||||
"admin.config.share.allow-registration.description": "Czy dozwolona jest rejestracja",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Zezwalaj na nieuwierzytelnione udostępnianie",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Czy nieautoryzowani użytkownicy mogą tworzyć udostępnienia",
|
||||
"admin.config.share.max-expiration": "Maksymalny okres ważności",
|
||||
"admin.config.share.max-expiration.description": "Maksymalny okres ważności udziału w godzinach. Ustaw na 0, aby zezwolić na nieograniczony okres ważności.",
|
||||
"admin.config.share.max-size": "Rozmiar maksymalny",
|
||||
"admin.config.share.max-size.description": "Maksymalny rozmiar udziału w bajtach",
|
||||
"admin.config.share.zip-compression-level": "Poziom kompresji Zip",
|
||||
"admin.config.share.zip-compression-level.description": "Dostosuj poziom do równowagi między rozmiarem pliku a szybkością kompresji. Prawidłowe wartości mieszczą się w zakresie od 0 do 9, przy czym 0 to brak kompresji a 9 maksymalną kompresją. ",
|
||||
"admin.config.smtp.enabled": "Włączony",
|
||||
"admin.config.smtp.enabled.description": "Czy SMTP ma zostać włączony. Ustaw to, tylko jeśli wprowadziłeś host, port, e-mail, nazwę użytkownika i hasło serwera SMTP.",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "Host serwera SMTP",
|
||||
"admin.config.smtp.port": "Numer portu",
|
||||
"admin.config.smtp.port.description": "Numer portu serwera SMTP",
|
||||
"admin.config.smtp.email": "Adres e-mail",
|
||||
"admin.config.smtp.email.description": "Adres e-mail, z którego e-maile są wysyłane",
|
||||
"admin.config.smtp.username": "Nazwa użytkownika",
|
||||
"admin.config.smtp.username.description": "Nazwa użytkownika serwera SMTP",
|
||||
"admin.config.smtp.password": "Hasło",
|
||||
"admin.config.smtp.password.description": "Hasło serwera SMTP",
|
||||
"admin.config.smtp.button.test": "Wyślij testowego e-maila",
|
||||
"admin.config.oauth.allow-registration": "Zezwól na rejestrację",
|
||||
"admin.config.oauth.allow-registration.description": "Zezwalaj użytkownikom na rejestrację za pomocą konta społecznościowego",
|
||||
"admin.config.oauth.ignore-totp": "Ignoruj TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Czy zignorować TOTP, kiedy użytkownik loguje się za pomocą konta społecznościowego",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Czy login na GitHub jest włączony",
|
||||
"admin.config.oauth.github-client-id": "ID klienta GitHub",
|
||||
"admin.config.oauth.github-client-id.description": "ID klienta aplikacji GitHub OAuth",
|
||||
"admin.config.oauth.github-client-secret": "Sekret klienta GitHub",
|
||||
"admin.config.oauth.github-client-secret.description": "Sekret klienta aplikacji GitHub OAuth",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Czy jest włączony login na GitHub",
|
||||
"admin.config.oauth.google-client-id": "ID klienta Google",
|
||||
"admin.config.oauth.google-client-id.description": "ID klienta aplikacji GitHub OAuth",
|
||||
"admin.config.oauth.google-client-secret": "Sekret klienta Google",
|
||||
"admin.config.oauth.google-client-secret.description": "Sekret klienta aplikacji Google OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Czy jest włączony login na GitHub",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID aplikacji Microsoft OAuth\nogólnie: Użytkownicy zarówno z osobistym kontem Microsoft, jak i kontem do pracy lub szkoły z Microsoft Entra ID mogą się zalogować do aplikacji. organizacje: Zalogować do aplikacji mogą tylko użytkownicy z kontami pracowniczymi lub szkolnymi z Microsoft Entra ID.\nkonsumenci: Zalogować się do aplikacji mogą tylko użytkownicy z osobistym kontem Microsoft.\nNazwa domeny lokatora Microsoft Entra lub identyfikator lokatora w formacie GUID: Zalogować się do aplikacji mogą tylko użytkownicy określonego najemcy Microsoft Entra (z listy członków na koncie służbowym lub szkolnym albo z listy gości na koncie osobistym Microsoft).",
|
||||
"admin.config.oauth.microsoft-client-id": "ID klienta Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description": "ID klienta aplikacji Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Sekret klienta Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Sekret klienta aplikacji Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Czy jest włączony login na Discord",
|
||||
"admin.config.oauth.discord-limited-guild": "ID ograniczonego serwera Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Ogranicz logowanie do użytkowników na określonym serwerze. Pozostaw puste, aby wyłączyć.",
|
||||
"admin.config.oauth.discord-client-id": "ID klienta Discord",
|
||||
"admin.config.oauth.discord-client-id.description": "ID klienta aplikacji Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "Sekret klienta Discord",
|
||||
"admin.config.oauth.discord-client-secret.description": "Sekret klienta aplikacji Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Czy jest włączony login OpenID Connect",
|
||||
"admin.config.oauth.oidc-discovery-uri": "Wykrywanie URI OpenID Connect",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Wykrywanie URI OAuth aplikacji OpenID Connect",
|
||||
"admin.config.oauth.oidc-username-claim": "Żądanie nazwy użytkownika OpenID Connect",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Żądanie nazwy użytkownika w tokenie identyfikatora OpenID Connect. Jeśli nie wiesz, czym jest ta konfiguracja, pozostaw pustą.",
|
||||
"admin.config.oauth.oidc-client-id": "Identyfikator klienta OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-id.description": "Identyfikator klienta OAuth aplikacji OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-secret": "Sekret klienta OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Sekret klienta OAuth aplikacji OpenID Connect",
|
||||
// 404
|
||||
"404.description": "Ups! Ta strona nie istnieje.",
|
||||
"404.button.home": "Wróć do strony domowej",
|
||||
// error
|
||||
"error.title": "Błąd",
|
||||
"error.description": "Ups!",
|
||||
"error.button.back": "Wróć",
|
||||
"error.msg.default": "Coś poszło nie tak.",
|
||||
"error.msg.access_denied": "Anulowałeś proces uwierzytelniania, spróbuj ponownie.",
|
||||
"error.msg.expired_token": "Proces uwierzytelniania trwał zbyt długo, spróbuj ponownie.",
|
||||
"error.msg.invalid_token": "Błąd wewnętrzny",
|
||||
"error.msg.no_user": "Użytkownik powiązany z tym kontem {0} nie istnieje.",
|
||||
"error.msg.no_email": "Nie można pobrać adresu e-mail z tego konta {0}.",
|
||||
"error.msg.already_linked": "To konto {0} zostało już połączone z innym kontem.",
|
||||
"error.msg.not_linked": "To konto {0} nie zostało jeszcze połączone z żadnym kontem.",
|
||||
"error.msg.unverified_account": "To konto {0} nie zostało zweryfikowane, spróbuj ponownie po weryfikacji.",
|
||||
"error.msg.discord_guild_permission_denied": "Nie możesz się zalogować.",
|
||||
"error.msg.cannot_get_user_info": "Nie można uzyskać informacji o użytkowniku z tego konta {0}.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Zapisz",
|
||||
"common.button.create": "Utwórz",
|
||||
"common.button.submit": "Prześlij",
|
||||
"common.button.delete": "Usuń",
|
||||
"common.button.cancel": "Anuluj",
|
||||
"common.button.confirm": "Potwierdź",
|
||||
"common.button.disable": "Wyłącz",
|
||||
"common.button.share": "Udostępnij",
|
||||
"common.button.generate": "Wygeneruj",
|
||||
"common.button.done": "Gotowe",
|
||||
"common.text.link": "Link",
|
||||
"common.text.or": "lub",
|
||||
"common.button.go-back": "Wróć",
|
||||
"common.notify.copied": "Link został skopiowany do schowka",
|
||||
"common.success": "Zakończono pomyślnie",
|
||||
"common.error": "Błąd",
|
||||
"common.error.unknown": "Wystąpił nieznany błąd",
|
||||
"common.error.invalid-email": "Adres e-mail jest nieprawidłowy",
|
||||
"common.error.too-short": "Musi mieć co najmniej {length} znaków",
|
||||
"common.error.too-long": "Musi mieć co najmniej {length} znaków",
|
||||
"common.error.exact-length": "Musi mieć dokładnie {length} znaków",
|
||||
"common.error.invalid-number": "Musi być liczbą",
|
||||
"common.error.field-required": "To pole jest wymagane"
|
||||
};
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"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",
|
||||
"signIn.oauth.or": "OU",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Criar uma conta",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Seu e-mail",
|
||||
"signup.button.submit": "Vamos começar",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Autenticação TOTP",
|
||||
"totp.button.signIn": "Iniciar sessão",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Esqueceu a sua senha?",
|
||||
"resetPassword.description": "Insira o seu e-mail para redefinir a sua senha.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Senha",
|
||||
"account.card.password.old": "Senha antiga",
|
||||
"account.card.password.new": "Nova senha",
|
||||
"account.card.password.noPasswordSet": "Você não tem uma senha definida. Se quiser entrar com e-mail e senha você precisa definir uma senha.",
|
||||
"account.notify.password.success": "Senha alterada com sucesso",
|
||||
"account.card.oauth.title": "Login com Redes Sociais",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Vincular",
|
||||
"account.card.oauth.unlink": "Desvincular",
|
||||
"account.card.oauth.unlinked": "Desvinculado",
|
||||
"account.modal.unlink.title": "Desvincular conta",
|
||||
"account.modal.unlink.description": "Desvincular suas redes sociais pode fazer com que você perca sua conta se não se lembrar de seu nome de usuário e senha.",
|
||||
"account.notify.oauth.unlinked.success": "Desvinculado 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",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"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.expires.error.too-long": "Expiração excede a data de expiração máxima de {max}.",
|
||||
"upload.modal.link.label": "Link",
|
||||
"upload.modal.expires.label": "Expiração",
|
||||
"upload.modal.expires.minute-singular": "Minuto",
|
||||
@@ -211,7 +235,7 @@ export default {
|
||||
"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.label": "Proteçã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",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Editar {shareId}",
|
||||
"share.edit.append-upload": "Anexar arquivo",
|
||||
"share.edit.notify.generic-error": "Ocorreu um erro ao terminar seu compartilhamento.",
|
||||
"share.edit.notify.save-success": "Compartilhamento atualizado com sucesso",
|
||||
// END /share/[id]/edit
|
||||
// /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.category.oauth": "Login com Redes Sociais",
|
||||
"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",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"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-expiration": "Expiração máxima",
|
||||
"admin.config.share.max-expiration.description": "Validade máxima de ações em horas. Defina 0 para permitir expiração ilimitada.",
|
||||
"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",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Senha",
|
||||
"admin.config.smtp.password.description": "Senha do servidor SMTP",
|
||||
"admin.config.smtp.button.test": "Enviar email de teste",
|
||||
"admin.config.oauth.allow-registration": "Permitir registro",
|
||||
"admin.config.oauth.allow-registration.description": "Permitir que os usuários se registrem através do login de redes sociais",
|
||||
"admin.config.oauth.ignore-totp": "Ignorar TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Ignorar o TOTP quando usuário usando login social",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Se o login GitHub está habilitado",
|
||||
"admin.config.oauth.github-client-id": "Client ID do GitHub",
|
||||
"admin.config.oauth.github-client-id.description": "ID do cliente do aplicativo GitHub OAuth",
|
||||
"admin.config.oauth.github-client-secret": "Client Secret do GitHub",
|
||||
"admin.config.oauth.github-client-secret.description": "ID do cliente do aplicativo GitHub OAuth",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Se o login GitHub está habilitado",
|
||||
"admin.config.oauth.google-client-id": "Client ID do Google",
|
||||
"admin.config.oauth.google-client-id.description": "ID do cliente do aplicativo GitHub OAuth",
|
||||
"admin.config.oauth.google-client-secret": "Client Secret do Google",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret do aplicativo GitHub OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Se o Microsoft login está habilitado",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "O ID do Tenant do app Microsoft OAuth\ncomum: Usuários com uma conta pessoal da Microsoft e uma conta de trabalho ou escola do Microsoft Entra podem entrar no aplicativo. organizações: somente usuários com contas de trabalho ou de escola do Microsoft Entra ID podem entrar no aplicativo.\nconsumidores: Somente usuários com uma conta pessoal da Microsoft podem acessar o aplicativo.\nnome de domínio do tenant Microsoft Entra ou do ID do inquilino no formato GUID: Somente usuários de um tenant Microsoft Entra específico (membros do diretório com uma conta de trabalho ou de diretório com uma conta pessoal da Microsoft) podem entrar no aplicativo.",
|
||||
"admin.config.oauth.microsoft-client-id": "ID do Cliente Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description": "ID do cliente do aplicativo Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Segredo do Cliente Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret do aplicativo Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Se o login do Discord está habilitado",
|
||||
"admin.config.oauth.discord-limited-guild": "ID do servidor limitado do Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limite de login para usuários em um servidor específico. Deixe em branco para desativar.",
|
||||
"admin.config.oauth.discord-client-id": "ID do Cliente Discord",
|
||||
"admin.config.oauth.discord-client-id.description": "ID do cliente do aplicativo Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "Segredo do Cliente Discord",
|
||||
"admin.config.oauth.discord-client-secret.description": "ID do cliente do aplicativo Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "Conectar o OpenID",
|
||||
"admin.config.oauth.oidc-enabled.description": "Se o login do OpenID Connect está ativado",
|
||||
"admin.config.oauth.oidc-discovery-uri": "URI de descoberta do OpenID Connect",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "URI da descoberta do aplicativo OpenID Connect OAuth",
|
||||
"admin.config.oauth.oidc-username-claim": "Reivindicação de nome de usuário OpenID Connect",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Nome de usuário no token de ID OpenID Connect. Deixe em branco se você não sabe o que é esta configuração.",
|
||||
"admin.config.oauth.oidc-client-id": "ID do cliente OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-id.description": "ID do cliente do aplicativo OpenID OAuth",
|
||||
"admin.config.oauth.oidc-client-secret": "Segredo do cliente OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-secret.description": "ID do cliente do aplicativo OpenID OAuth",
|
||||
// 404
|
||||
"404.description": "Ops, esta página não existe.",
|
||||
"404.button.home": "Me traga de volta para casa",
|
||||
// error
|
||||
"error.title": "Erro",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Voltar",
|
||||
"error.msg.default": "Algo deu errado.",
|
||||
"error.msg.access_denied": "Você cancelou o processo de autenticação, por favor, tente novamente.",
|
||||
"error.msg.expired_token": "O processo de autenticação demorou muito. Por favor, tente novamente.",
|
||||
"error.msg.invalid_token": "Erro interno",
|
||||
"error.msg.no_user": "O usuário vinculado a esta conta {0} não existe.",
|
||||
"error.msg.no_email": "Não é possível obter o endereço de e-mail desta conta {0}.",
|
||||
"error.msg.already_linked": "Esta conta {0} já está vinculada a outra conta.",
|
||||
"error.msg.not_linked": "Esta conta {0} ainda não foi vinculada a nenhuma conta.",
|
||||
"error.msg.unverified_account": "Esta conta {0} não foi verificada, tente novamente após a verificação.",
|
||||
"error.msg.discord_guild_permission_denied": "Você não tem permissão para acessar.",
|
||||
"error.msg.cannot_get_user_info": "Não é possível obter suas informações de usuário desta conta {0}.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Salvar",
|
||||
"common.button.create": "Criar",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "Вход",
|
||||
"signIn.notify.totp-required.title": "Требуется двухфакторная аутентификация",
|
||||
"signIn.notify.totp-required.description": "Пожалуйста, введите код Вашей 2-х факторной аутентификации",
|
||||
"signIn.oauth.or": "ИЛИ",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Создать аккаунт",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Адрес эл. почты",
|
||||
"signup.button.submit": "Давайте начнём",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "Авторизация TOTP",
|
||||
"totp.button.signIn": "Войти",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Забыли пароль?",
|
||||
"resetPassword.description": "Введите ваш email для восстановления пароля.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Пароль",
|
||||
"account.card.password.old": "Старый пароль",
|
||||
"account.card.password.new": "Новый пароль",
|
||||
"account.card.password.noPasswordSet": "У вас не установлен пароль. Если вы хотите войти с помощью электронной почты и пароля, вам необходимо установить пароль.",
|
||||
"account.notify.password.success": "Пароль успешно изменён",
|
||||
"account.card.oauth.title": "Авторизация через социальные сети",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Подключить",
|
||||
"account.card.oauth.unlink": "Отключить",
|
||||
"account.card.oauth.unlinked": "Отключен",
|
||||
"account.modal.unlink.title": "Отключить связь с учетной записью",
|
||||
"account.modal.unlink.description": "Отключение связи с учетной записью социальных аккаунтов может привести к потере вашей учетной записи, если вы не помните свое имя пользователя и пароль.",
|
||||
"account.notify.oauth.unlinked.success": "Отключение прошло успешно",
|
||||
"account.card.security.title": "Безопасность",
|
||||
"account.card.security.totp.enable.description": "Введите ваш текущий пароль для начала включения TOTP",
|
||||
"account.card.security.totp.disable.description": "Введите ваш текущий пароль, чтобы отключить TOTP",
|
||||
@@ -111,7 +134,7 @@ export default {
|
||||
"account.reverseShares.title.empty": "Тут пусто 👀",
|
||||
"account.reverseShares.description.empty": "У вас пока нет обратных загрузок.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"account.reverseShares.modal.title": "Создать обратную ссылку на файл",
|
||||
"account.reverseShares.modal.expiration.label": "Истекает",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Минута",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Минут(ы)",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"upload.modal.not-signed-in-description": "Вы не сможете удалить свои файлы вручную и просмотреть количество посетителей.",
|
||||
"upload.modal.expires.never": "никогда",
|
||||
"upload.modal.expires.never-long": "Никогда не истекает",
|
||||
"upload.modal.expires.error.too-long": "Срок действия превышает максимальную дату окончания срока действия {max}.",
|
||||
"upload.modal.link.label": "Ссылка",
|
||||
"upload.modal.expires.label": "Истекает",
|
||||
"upload.modal.expires.minute-singular": "Минута",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"share.modal.file-preview.error.not-supported.title": "Предпросмотр не поддерживается",
|
||||
"share.modal.file-preview.error.not-supported.description": "Предварительный просмотр этого типа файла не поддерживается. Пожалуйста, скачайте файл для его просмотра.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Редактировать {shareId}",
|
||||
"share.edit.append-upload": "Добавить файл",
|
||||
"share.edit.notify.generic-error": "Произошла ошибка при завершении вашей загрузки.",
|
||||
"share.edit.notify.save-success": "Ссылка на ресурс успешна обновлена",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Конфигурация",
|
||||
"admin.config.category.general": "Общее",
|
||||
"admin.config.category.share": "Загрузки",
|
||||
"admin.config.category.email": "Электронная почта",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Авторизация через социальные сети",
|
||||
"admin.config.general.app-name": "Название приложения",
|
||||
"admin.config.general.app-name.description": "Видимое название приложения",
|
||||
"admin.config.general.app-url": "URL-адрес приложения",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"admin.config.share.allow-registration.description": "Разрешена ли регистрация",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Разрешить неавторизованные загрузки",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Могут ли неавторизованные пользователи создавать загрузки",
|
||||
"admin.config.share.max-expiration": "Максимальная срок действия",
|
||||
"admin.config.share.max-expiration.description": "Максимальный срок действия общего доступа в часах. Установите значение 0, чтобы разрешить неограниченный срок действия.",
|
||||
"admin.config.share.max-size": "Максимальный размер",
|
||||
"admin.config.share.max-size.description": "Максимальный размер файла в байтах",
|
||||
"admin.config.share.zip-compression-level": "Уровень сжатия Zip",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Пароль",
|
||||
"admin.config.smtp.password.description": "Пароль SMTP-сервера",
|
||||
"admin.config.smtp.button.test": "Отправить тестовое письмо",
|
||||
"admin.config.oauth.allow-registration": "Разрешить регистрацию",
|
||||
"admin.config.oauth.allow-registration.description": "Разрешить пользователям регистрироваться используя учетные записи социальных сетей",
|
||||
"admin.config.oauth.ignore-totp": "Игнорировать TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Игнорировать TOTP при использовании социальной авторизации",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Включен ли логин на GitHub",
|
||||
"admin.config.oauth.github-client-id": "ID клиента GitHub",
|
||||
"admin.config.oauth.github-client-id.description": "ID клиента в приложении GitHub OAuth",
|
||||
"admin.config.oauth.github-client-secret": "Секретный ключ клиента GitHub",
|
||||
"admin.config.oauth.github-client-secret.description": "Секретный ключ клиента в приложении GitHub OAuth",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Включен ли логин Google на GitHub",
|
||||
"admin.config.oauth.google-client-id": "ID клиента Google",
|
||||
"admin.config.oauth.google-client-id.description": "ID клиента в приложении Google OAuth",
|
||||
"admin.config.oauth.google-client-secret": "Секретный ключ клиента Google",
|
||||
"admin.config.oauth.google-client-secret.description": "Секретный ключ клиента в приложении Google OAuth",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Включен ли логин Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant": "Корпоративный аккаунт Microsoft",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Идентификатор арендатора приложения Microsoft OAuth\ncommon: Пользователи с личным аккаунтом Microsoft и рабочим или учебным аккаунтом от Microsoft Entra ID могут войти в приложение. organizations: Только пользователи с рабочим или учебным аккаунтом от Microsoft Entra ID могут войти в приложение.\nconsumers: Только пользователи с личным аккаунтом Microsoft могут войти в приложение.\nимя домена арендатора Microsoft Entra или идентификатор арендатора в формате GUID: Только пользователи из определенного арендатора Microsoft Entra (участники каталога с рабочим или учебным аккаунтом или гости каталога с личным аккаунтом Microsoft) могут войти в приложение.",
|
||||
"admin.config.oauth.microsoft-client-id": "Идентификатор клиента Microsoft",
|
||||
"admin.config.oauth.microsoft-client-id.description": "ID клиента в приложении Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Секретный ключ клиента Microsoft",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Секретный ключ клиента в приложении Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Включен ли логин Discord",
|
||||
"admin.config.oauth.discord-limited-guild": "ID ограниченного сервера Discord",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Ограничение входа для пользователей определенного сервера. Оставьте пустым, чтобы отключить.",
|
||||
"admin.config.oauth.discord-client-id": "ID клиента Discord",
|
||||
"admin.config.oauth.discord-client-id.description": "ID клиента в приложении Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "Секретный ключ клиента Discord",
|
||||
"admin.config.oauth.discord-client-secret.description": "Секретный ключ клиента в приложении Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Упс, этой страницы не существует.",
|
||||
"404.button.home": "Верните меня домой",
|
||||
// error
|
||||
"error.title": "Ошибка",
|
||||
"error.description": "Что-то пошло не так!",
|
||||
"error.button.back": "Назад",
|
||||
"error.msg.default": "Что-то пошло не так.",
|
||||
"error.msg.access_denied": "Вы отменили процесс аутентификации, пожалуйста, попробуйте еще раз.",
|
||||
"error.msg.expired_token": "Процесс аутентификации занял слишком много времени, пожалуйста, попробуйте еще раз.",
|
||||
"error.msg.invalid_token": "Внутренняя ошибка",
|
||||
"error.msg.no_user": "Пользователь связанный с учетной записью {0} не существует.",
|
||||
"error.msg.no_email": "Не удается получить адрес электронной почты от учетной записи {0}.",
|
||||
"error.msg.already_linked": "Эта учетная запись {0} уже привязана к другому аккаунту.",
|
||||
"error.msg.not_linked": "Эта учетная запись {0} ещё не привязана ни к одному аккаунту.",
|
||||
"error.msg.unverified_account": "Эта учетная запись {0} не подтверждена, повторите попытку после подтверждения.",
|
||||
"error.msg.discord_guild_permission_denied": "У вас нет разрешения на вход.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Сохранить",
|
||||
"common.button.create": "Создать",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "Пријавите се",
|
||||
"signIn.notify.totp-required.title": "Потребна је двофакторска аутентификација",
|
||||
"signIn.notify.totp-required.description": "Унесите свој двофакторски код за аутентификацију",
|
||||
"signIn.oauth.or": "Или",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Направи налог",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "Ваш и-мејл",
|
||||
"signup.button.submit": "Хајде да почнемо",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "ТОТП аутентификација",
|
||||
"totp.button.signIn": "Пријавите се",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Заборавили сте лозинку?",
|
||||
"resetPassword.description": "Унесите своју е-пошту да бисте ресетовали лозинку.",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "Лозинка",
|
||||
"account.card.password.old": "Стара лозинка",
|
||||
"account.card.password.new": "Нова лозинка",
|
||||
"account.card.password.noPasswordSet": "Немате постављену лозинку. Ако желите да се пријавите помоћу е-поште и лозинке, потребно је да поставите лозинку.",
|
||||
"account.notify.password.success": "Лозинка је успешно промењена",
|
||||
"account.card.oauth.title": "Социал логин",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Прекини везу",
|
||||
"account.card.oauth.unlinked": "Веза је прекинута",
|
||||
"account.modal.unlink.title": "Прекини везу са налогом",
|
||||
"account.modal.unlink.description": "Прекидање везе са вашим друштвеним налозима може довести до губитка налога ако се не сећате корисничког имена и лозинке.",
|
||||
"account.notify.oauth.unlinked.success": "Веза је прекинута",
|
||||
"account.card.security.title": "Безбедност",
|
||||
"account.card.security.totp.enable.description": "Унесите своју тренутну лозинку да бисте почели да омогућавате ТОТП",
|
||||
"account.card.security.totp.disable.description": "Унесите своју тренутну лозинку да бисте онемогућили ТОТП",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"upload.modal.not-signed-in-description": "Нећете моћи ручно да избришете своје дељење и видите број посетилаца.",
|
||||
"upload.modal.expires.never": "никад",
|
||||
"upload.modal.expires.never-long": "Никада не истиче",
|
||||
"upload.modal.expires.error.too-long": "Истек премашује максимални датум истека од {max}.",
|
||||
"upload.modal.link.label": "Линк",
|
||||
"upload.modal.expires.label": "Истиче",
|
||||
"upload.modal.expires.minute-singular": "Минут",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"share.modal.file-preview.error.not-supported.title": "Преглед није подржан",
|
||||
"share.modal.file-preview.error.not-supported.description": "Преглед за овај тип датотеке није подржан. Преузмите датотеку да бисте је видели.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Конфигурација",
|
||||
"admin.config.category.general": "Опште",
|
||||
"admin.config.category.share": "Дељење",
|
||||
"admin.config.category.email": "E-пошта",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Социал Логин",
|
||||
"admin.config.general.app-name": "Назив апликације",
|
||||
"admin.config.general.app-name.description": "Назив апликације",
|
||||
"admin.config.general.app-url": "URL апликације",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"admin.config.share.allow-registration.description": "Да ли је регистрација дозвољена",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Дозволи дељење без аутентификације",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Да ли корисници без аутентификације могу да креирају дељења",
|
||||
"admin.config.share.max-expiration": "Максимални рок трајања",
|
||||
"admin.config.share.max-expiration.description": "Максимални рок трајања дељења у сатима. Поставите на 0 да бисте омогућили неограничено трајање.",
|
||||
"admin.config.share.max-size": "Максимална величина",
|
||||
"admin.config.share.max-size.description": "Максимална величина дељења у бајтовима",
|
||||
"admin.config.share.zip-compression-level": "Ниво zip компресије",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "Лозинка",
|
||||
"admin.config.smtp.password.description": "Лозинка SMTP сервера",
|
||||
"admin.config.smtp.button.test": "Пошаљи тестну е-пошту",
|
||||
"admin.config.oauth.allow-registration": "Дозволи регистрацију",
|
||||
"admin.config.oauth.allow-registration.description": "Дозволите корисницима да се региструју путем друштвене пријаве",
|
||||
"admin.config.oauth.ignore-totp": "Занемари ТОТП",
|
||||
"admin.config.oauth.ignore-totp.description": "Да ли да игноришете ТОТП када корисник користи пријаву на друштвеним мрежама",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Да ли је пријављивање на GitHub омогућено",
|
||||
"admin.config.oauth.github-client-id": "GitHub ИД клијента",
|
||||
"admin.config.oauth.github-client-id.description": "ИД клијента GitHub OAuth апликације",
|
||||
"admin.config.oauth.github-client-secret": "Тајна GitHub клијента",
|
||||
"admin.config.oauth.github-client-secret.description": "Тајна клијента GitHub OAuth апликације",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Да ли је пријављивање на Google омогућено",
|
||||
"admin.config.oauth.google-client-id": "ИД Google клијента",
|
||||
"admin.config.oauth.google-client-id.description": "ИД клијента Google OAuth апликације",
|
||||
"admin.config.oauth.google-client-secret": "Тајна Google клијента",
|
||||
"admin.config.oauth.google-client-secret.description": "Тајна клијента Google OAuth апликације",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Да ли је пријављивање на Microsoft омогућено",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "ИД закупца за Microsoft OAuth апликацију\nуобичајено: Корисници са личним Microsoft налогом и пословним или школским налогом са Microsoft Entra ID-а могу да се пријаве у апликацију. организације: Само корисници са пословним или школским налозима са Microsoft Entra ID-а могу да се пријаве у апликацију.\nпотрошачи: Само корисници са личним Microsoft налогом могу да се пријаве у апликацију.\nиме домена закупца Microsoft Entra ID или ИД закупца у ГУИД формату: Само корисници из одређеног Microsoft Entra ID станара (чланови директоријума са пословним или школским налогом или гости директоријума са личним Microsoft налогом) могу да се пријаве у апликацију.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsotf Цлиент ИД",
|
||||
"admin.config.oauth.microsoft-client-id.description": "ИД клијента Microsoft OAuth апликације",
|
||||
"admin.config.oauth.microsoft-client-secret": "Тајна Microsoft клијента",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Тајна клијента за Microsot OAuth апликацију",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Да ли је пријављивање на Discord омогућено",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord ИД клијента",
|
||||
"admin.config.oauth.discord-client-id.description": "ИД клијента Discord OAuth апликације",
|
||||
"admin.config.oauth.discord-client-secret": "Discord клијент тајна",
|
||||
"admin.config.oauth.discord-client-secret.description": "Тајна клијента Discord OAuth апликације",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Опа - Ова страна не постоји.",
|
||||
"404.button.home": "Врати ме на почетак",
|
||||
// error
|
||||
"error.title": "Грешка",
|
||||
"error.description": "Ooпс!",
|
||||
"error.button.back": "Иди назад",
|
||||
"error.msg.default": "Нешто је пошло наопако.",
|
||||
"error.msg.access_denied": "Отказали сте процес аутентификације, покушајте поново.",
|
||||
"error.msg.expired_token": "Процес аутентификације је трајао предуго, покушајте поново.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "Корисник повезан са овим {0} налогом не постоји.",
|
||||
"error.msg.no_email": "Не могу да добијем адресу е-поште са овог {0} налога.",
|
||||
"error.msg.already_linked": "Овај {0} налог је већ повезан са другим налогом.",
|
||||
"error.msg.not_linked": "Овај {0} налог још увек није повезан ни са једним налогом.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Сачувај",
|
||||
"common.button.create": "Направи",
|
||||
|
||||
419
frontend/src/i18n/translations/sv-SE.ts
Normal file
419
frontend/src/i18n/translations/sv-SE.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
export default {
|
||||
// Navbar
|
||||
"navbar.upload": "Ladda upp",
|
||||
"navbar.signin": "Logga in",
|
||||
"navbar.home": "Startsida",
|
||||
"navbar.signup": "Skapa konto",
|
||||
"navbar.links.shares": "Mina delningar",
|
||||
"navbar.links.reverse": "Omvända delningar",
|
||||
"navbar.avatar.account": "Mitt konto",
|
||||
"navbar.avatar.admin": "Administration",
|
||||
"navbar.avatar.signout": "Logga ut",
|
||||
// END navbar
|
||||
// /
|
||||
"home.title": "En <h>egen</h> fildelningsplattform.",
|
||||
"home.description": "Vill du verkligen lägga dina personliga filer hos en tredje part som WeTransfer?",
|
||||
"home.bullet.a.name": "Lokalt installerad",
|
||||
"home.bullet.a.description": "Hosta Pingvin Share på din egen maskin.",
|
||||
"home.bullet.b.name": "Sekretess",
|
||||
"home.bullet.b.description": "Dina filer är dina filer och bör aldrig hamna i händerna på tredje part.",
|
||||
"home.bullet.c.name": "Ingen irriterande filstorleksbegränsning",
|
||||
"home.bullet.c.description": "Ladda upp så stora filer som du vill. Endast din hårddisk kommer att vara din gräns.",
|
||||
"home.button.start": "Kom igång",
|
||||
"home.button.source": "Källkod",
|
||||
// END /
|
||||
// /auth/signin
|
||||
"signin.title": "Välkommen tillbaka",
|
||||
"signin.description": "Har du inget konto än?",
|
||||
"signin.button.signup": "Skapa konto",
|
||||
"signin.input.email-or-username": "E-post eller användarnamn",
|
||||
"signin.input.email-or-username.placeholder": "Din e-postadress eller ditt användarnamn",
|
||||
"signin.input.password": "Lösenord",
|
||||
"signin.input.password.placeholder": "Lösenord",
|
||||
"signin.button.submit": "Logga in",
|
||||
"signIn.notify.totp-required.title": "Tvåfaktorsautentisering krävs",
|
||||
"signIn.notify.totp-required.description": "Vänligen ange din tvåfaktorsautentiseringskod",
|
||||
"signIn.oauth.or": "ELLER",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "Skapa ett konto",
|
||||
"signup.description": "Har du redan ett konto?",
|
||||
"signup.button.signin": "Logga in",
|
||||
"signup.input.username": "Användarnamn",
|
||||
"signup.input.username.placeholder": "Ditt användarnamn",
|
||||
"signup.input.email": "E-post",
|
||||
"signup.input.email.placeholder": "Din e-post",
|
||||
"signup.button.submit": "Kom igång",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP-autentisering",
|
||||
"totp.button.signIn": "Logga in",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "Glömt ditt lösenord?",
|
||||
"resetPassword.description": "Ange din e-postadress för att återställa ditt lösenord.",
|
||||
"resetPassword.notify.success": "Ett e-postmeddelande har skickats till dig med en länk för att återställa lösenordet.",
|
||||
"resetPassword.button.back": "Tillbaka till inloggningssidan",
|
||||
"resetPassword.text.resetPassword": "Återställ lösenord",
|
||||
"resetPassword.text.enterNewPassword": "Ange ditt nya lösenord",
|
||||
"resetPassword.input.password": "Nytt lösenord",
|
||||
"resetPassword.notify.passwordReset": "Ditt lösenord har återställts.",
|
||||
// /account
|
||||
"account.title": "Mitt konto",
|
||||
"account.card.info.title": "Kontoinformation",
|
||||
"account.card.info.username": "Användarnamn",
|
||||
"account.card.info.email": "E-post",
|
||||
"account.notify.info.success": "Kontot uppdaterades",
|
||||
"account.card.password.title": "Lösenord",
|
||||
"account.card.password.old": "Gammalt lösenord",
|
||||
"account.card.password.new": "Nytt lösenord",
|
||||
"account.card.password.noPasswordSet": "Du har inget lösenord. Om du vill logga in med e-post och lösenord måste du ange ett lösenord.",
|
||||
"account.notify.password.success": "Lösenordet har ändrats",
|
||||
"account.card.oauth.title": "Inloggning via sociala nätverk",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Länka",
|
||||
"account.card.oauth.unlink": "Avlänka",
|
||||
"account.card.oauth.unlinked": "Avlänkad",
|
||||
"account.modal.unlink.title": "Avlänka konto",
|
||||
"account.modal.unlink.description": "Om du kopplar bort dina sociala konton kan det leda till att du förlorar ditt konto om du inte kommer ihåg ditt användarnamn och lösenord.",
|
||||
"account.notify.oauth.unlinked.success": "Avlänkning utförd",
|
||||
"account.card.security.title": "Säkerhet",
|
||||
"account.card.security.totp.enable.description": "Ange ditt nuvarande lösenord för att aktivera TOTP",
|
||||
"account.card.security.totp.disable.description": "Ange ditt lösenord för att inaktivera TOTP",
|
||||
"account.card.security.totp.button.start": "Start",
|
||||
"account.modal.totp.title": "Aktivera TOTP",
|
||||
"account.modal.totp.step1": "Steg 1: Lägg till din autentiserare",
|
||||
"account.modal.totp.step2": "Steg 2: Bekräfta din kod",
|
||||
"account.modal.totp.enterManually": "Ange manuellt",
|
||||
"account.modal.totp.code": "Kod",
|
||||
"account.modal.totp.clickToCopy": "Klicka för att kopiera",
|
||||
"account.modal.totp.verify": "Verifiera",
|
||||
"account.notify.totp.disable": "TOTP har inaktiverats",
|
||||
"account.notify.totp.enable": "TOTP aktiverat",
|
||||
"account.card.language.title": "Språk",
|
||||
"account.card.language.description": "Projektet är översatt av gemenskapen. Vissa översättningar kan vara ofullständiga.",
|
||||
"account.card.color.title": "Färgschema",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "Mörk",
|
||||
"account.theme.light": "Ljus",
|
||||
"account.theme.system": "System",
|
||||
"account.button.delete": "Ta bort konto",
|
||||
"account.modal.delete.title": "Ta bort konto",
|
||||
"account.modal.delete.description": "Vill du verkligen ta bort ditt konto inklusive alla dina aktiva delningar?",
|
||||
// END /account
|
||||
// /account/shares
|
||||
"account.shares.title": "Mina delningar",
|
||||
"account.shares.title.empty": "Här var det tomt 👀",
|
||||
"account.shares.description.empty": "Du har inga delningar.",
|
||||
"account.shares.button.create": "Skapa en",
|
||||
"account.shares.info.title": "Information om delning",
|
||||
"account.shares.table.id": "ID",
|
||||
"account.shares.table.name": "Namn",
|
||||
"account.shares.table.description": "Beskrivning",
|
||||
"account.shares.table.visitors": "Besökare",
|
||||
"account.shares.table.expiresAt": "Förfaller den",
|
||||
"account.shares.table.createdAt": "Skapad den",
|
||||
"account.shares.table.size": "Storlek",
|
||||
"account.shares.modal.share-informations": "Information om delning",
|
||||
"account.shares.modal.share-link": "Delningslänk",
|
||||
"account.shares.modal.delete.title": "Ta bort delning {share}",
|
||||
"account.shares.modal.delete.description": "Vill du verkligen ta bort denna delning?",
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "Omvända delningar",
|
||||
"account.reverseShares.description": "En omvänd delning gör att du kan generera en unik URL som tillåter externa användare att skapa en delning.",
|
||||
"account.reverseShares.title.empty": "Här var det tomt 👀",
|
||||
"account.reverseShares.description.empty": "Du har inga omvända delningar.",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Skapa omvänd delning",
|
||||
"account.reverseShares.modal.expiration.label": "Förfaller",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "Minut",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "Minuter",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "Timme",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "Timmar",
|
||||
"account.reverseShares.modal.expiration.day-singular": "Dag",
|
||||
"account.reverseShares.modal.expiration.day-plural": "Dagar",
|
||||
"account.reverseShares.modal.expiration.week-singular": "Vecka",
|
||||
"account.reverseShares.modal.expiration.week-plural": "Veckor",
|
||||
"account.reverseShares.modal.expiration.month-singular": "Månad",
|
||||
"account.reverseShares.modal.expiration.month-plural": "Månader",
|
||||
"account.reverseShares.modal.expiration.year-singular": "År",
|
||||
"account.reverseShares.modal.expiration.year-plural": "År",
|
||||
"account.reverseShares.modal.max-size.label": "Max storlek på delning",
|
||||
"account.reverseShares.modal.send-email": "Skicka e-postavisering",
|
||||
"account.reverseShares.modal.send-email.description": "Skicka ett e-postmeddelande när en delning skapas med denna länk för omvänd delning.",
|
||||
"account.reverseShares.modal.max-use.label": "Maxanvändningar",
|
||||
"account.reverseShares.modal.max-use.description": "Den maximala mängden gånger denna URL kan användas för att skapa en delning.",
|
||||
"account.reverseShare.never-expires": "Denna omvända delning kommer aldrig att förfalla.",
|
||||
"account.reverseShare.expires-on": "Denna sammanlagda delning löper ut på {expiration}.",
|
||||
"account.reverseShares.table.no-shares": "Inga delningar har skapats ännu",
|
||||
"account.reverseShares.table.count.singular": "delning",
|
||||
"account.reverseShares.table.count.plural": "delningar",
|
||||
"account.reverseShares.table.shares": "Delningar",
|
||||
"account.reverseShares.table.remaining": "Återstående användningar",
|
||||
"account.reverseShares.table.max-size": "Max storlek på delning",
|
||||
"account.reverseShares.table.expires": "Förfaller den",
|
||||
"account.reverseShares.modal.reverse-share-link": "Omvänd delningslänk",
|
||||
"account.reverseShares.modal.delete.title": "Ta bort omvänd delning",
|
||||
"account.reverseShares.modal.delete.description": "Vill du verkligen ta bort denna omvänd delning? Om du gör det, kommer de tillhörande delningarna också att raderas.",
|
||||
// END /account/reverseShares
|
||||
// /admin
|
||||
"admin.title": "Administration",
|
||||
"admin.button.users": "Användarhantering",
|
||||
"admin.button.config": "Konfiguration",
|
||||
"admin.version": "Version",
|
||||
// END /admin
|
||||
// /admin/users
|
||||
"admin.users.title": "Användarhantering",
|
||||
"admin.users.table.username": "Användarnamn",
|
||||
"admin.users.table.email": "E-post",
|
||||
"admin.users.table.admin": "Administratör",
|
||||
"admin.users.edit.update.title": "Uppdatera användare {username}",
|
||||
"admin.users.edit.update.admin-privileges": "Administratörsbehörigheter",
|
||||
"admin.users.edit.update.change-password.title": "Ändra lösenord",
|
||||
"admin.users.edit.update.change-password.field": "Nytt lösenord",
|
||||
"admin.users.edit.update.change-password.button": "Spara nytt lösenord",
|
||||
"admin.users.edit.update.notify.password.success": "Lösenordet har ändrats",
|
||||
"admin.users.edit.delete.title": "Ta bort användare {username}",
|
||||
"admin.users.edit.delete.description": "Vill du verkligen ta bort denna användare och alla deras delningar?",
|
||||
// showCreateUserModal.tsx
|
||||
"admin.users.modal.create.title": "Skapa användare",
|
||||
"admin.users.modal.create.username": "Användarnamn",
|
||||
"admin.users.modal.create.email": "E-post",
|
||||
"admin.users.modal.create.password": "Lösenord",
|
||||
"admin.users.modal.create.manual-password": "Sätt lösenord manuellt",
|
||||
"admin.users.modal.create.manual-password.description": "Om den inte är markerad kommer användaren att få ett e-postmeddelande med en länk för att ange lösenordet.",
|
||||
"admin.users.modal.create.admin": "Administratörsbehörigheter",
|
||||
"admin.users.modal.create.admin.description": "Om detta markeras kommer användaren att kunna komma åt administratörspanelen.",
|
||||
// END /admin/users
|
||||
// /upload
|
||||
"upload.title": "Ladda upp",
|
||||
"upload.notify.generic-error": "Ett fel uppstod när din delning skulle slutföras.",
|
||||
"upload.notify.count-failed": "{count} filer kunde inte laddas upp. Försöker igen.",
|
||||
// Dropzone.tsx
|
||||
"upload.dropzone.title": "Ladda upp filer",
|
||||
"upload.dropzone.description": "Drag och släpp filer här för att starta din delning. Vi kan endast acceptera filer som är mindre än {maxSize} totalt.",
|
||||
"upload.dropzone.notify.file-too-big": "Dina filer överskrider den maximala storleken på {maxSize}.",
|
||||
// FileList.tsx
|
||||
"upload.filelist.name": "Namn",
|
||||
"upload.filelist.size": "Storlek",
|
||||
// showCreateUploadModal.tsx
|
||||
"upload.modal.title": "Skapa delning",
|
||||
"upload.modal.link.error.invalid": "Kan endast innehålla bokstäver, siffror, understreck och bindestreck",
|
||||
"upload.modal.link.error.taken": "Denna länk används redan",
|
||||
"upload.modal.not-signed-in": "Du är inte inloggad",
|
||||
"upload.modal.not-signed-in-description": "Du kommer inte att kunna ta bort din delning manuellt och visa antalet besökare.",
|
||||
"upload.modal.expires.never": "aldrig",
|
||||
"upload.modal.expires.never-long": "Upphör aldrig",
|
||||
"upload.modal.expires.error.too-long": "Förfallodatum överskrider maximalt utgångsdatum för {max}.",
|
||||
"upload.modal.link.label": "Länk",
|
||||
"upload.modal.expires.label": "Förfaller",
|
||||
"upload.modal.expires.minute-singular": "Minut",
|
||||
"upload.modal.expires.minute-plural": "Minuter",
|
||||
"upload.modal.expires.hour-singular": "Timme",
|
||||
"upload.modal.expires.hour-plural": "Timmar",
|
||||
"upload.modal.expires.day-singular": "Dag",
|
||||
"upload.modal.expires.day-plural": "Dagar",
|
||||
"upload.modal.expires.week-singular": "Vecka",
|
||||
"upload.modal.expires.week-plural": "Veckor",
|
||||
"upload.modal.expires.month-singular": "Månad",
|
||||
"upload.modal.expires.month-plural": "Månader",
|
||||
"upload.modal.expires.year-singular": "År",
|
||||
"upload.modal.expires.year-plural": "År",
|
||||
"upload.modal.accordion.description.title": "Beskrivning",
|
||||
"upload.modal.accordion.description.placeholder": "Anteckning till mottagare av denna delning",
|
||||
"upload.modal.accordion.email.title": "E-postmottagare",
|
||||
"upload.modal.accordion.email.placeholder": "Ange e-postmottagare",
|
||||
"upload.modal.accordion.email.invalid-email": "Ogiltig e-postadress",
|
||||
"upload.modal.accordion.security.title": "Säkerhetsalternativ",
|
||||
"upload.modal.accordion.security.password.label": "Lösenordsskydd",
|
||||
"upload.modal.accordion.security.password.placeholder": "Inget lösenord",
|
||||
"upload.modal.accordion.security.max-views.label": "Max antal visningar",
|
||||
"upload.modal.accordion.security.max-views.placeholder": "Ingen gräns",
|
||||
// showCompletedUploadModal.tsx
|
||||
"upload.modal.completed.never-expires": "Denna delning kommer aldrig att upphöra.",
|
||||
"upload.modal.completed.expires-on": "Denna delning upphör att gälla {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Delning redo",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "Delning {shareId}",
|
||||
"share.description": "Titta vad jag har delat med dig!",
|
||||
"share.error.visitor-limit-exceeded.title": "Besökargränsen överskriden",
|
||||
"share.error.visitor-limit-exceeded.description": "Gränsen för antalet besökare för denna delning har överskridits.",
|
||||
"share.error.removed.title": "Delning borttagen",
|
||||
"share.error.not-found.title": "Delningen hittades inte",
|
||||
"share.error.not-found.description": "Delningen du letar efter existerar inte.",
|
||||
"share.modal.password.title": "Lösenord krävs",
|
||||
"share.modal.password.description": "För att komma åt denna delning, vänligen ange lösenordet för delningen.",
|
||||
"share.modal.password": "Lösenord",
|
||||
"share.modal.error.invalid-password": "Ogiltigt lösenord",
|
||||
"share.button.download-all": "Ladda ner allt",
|
||||
"share.notify.download-all-preparing": "Delningen förbereds. Försök igen om några minuter.",
|
||||
"share.modal.file-link": "Fillänk",
|
||||
"share.table.name": "Namn",
|
||||
"share.table.size": "Storlek",
|
||||
"share.modal.file-preview.error.not-supported.title": "Förhandsgranskning stöds ej",
|
||||
"share.modal.file-preview.error.not-supported.description": "En förhandsvisning för filtypen stöds inte. Ladda ner filen för att se den.",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Redigera {shareId}",
|
||||
"share.edit.append-upload": "Lägg till fil",
|
||||
"share.edit.notify.generic-error": "Ett fel uppstod när din delning skulle slutföras.",
|
||||
"share.edit.notify.save-success": "Delningen har uppdaterats",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "Konfiguration",
|
||||
"admin.config.category.general": "Allmänt",
|
||||
"admin.config.category.share": "Delning",
|
||||
"admin.config.category.email": "E-post",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Inloggning via sociala nätverk",
|
||||
"admin.config.general.app-name": "Appnamn",
|
||||
"admin.config.general.app-name.description": "Namn på applikationen",
|
||||
"admin.config.general.app-url": "Appens URL",
|
||||
"admin.config.general.app-url.description": "På vilken URL Pingvin Share finns",
|
||||
"admin.config.general.show-home-page": "Visa startsidan",
|
||||
"admin.config.general.show-home-page.description": "Om du vill visa startsidan",
|
||||
"admin.config.general.logo": "Logotyp",
|
||||
"admin.config.general.logo.description": "Ändra din logotyp genom att ladda upp en ny bild. Bilden måste vara en PNG och bör ha formatet 1:1.",
|
||||
"admin.config.general.logo.placeholder": "Välj bild",
|
||||
"admin.config.email.enable-share-email-recipients": "Aktivera e-postmottagare för delning",
|
||||
"admin.config.email.enable-share-email-recipients.description": "Om du vill tillåta e-post till delningsmottagare. Aktivera endast detta om du har aktiverat SMTP.",
|
||||
"admin.config.email.share-recipients-subject": "Delning ämne",
|
||||
"admin.config.email.share-recipients-subject.description": "Ämne för e-postmeddelandet som skickas till delningsmottagarna.",
|
||||
"admin.config.email.share-recipients-message": "Delning meddelande",
|
||||
"admin.config.email.share-recipients-message.description": "Meddelande som skickas till delningens mottagare. Tillgängliga variabler:\n {creator} - Användarnamnet för skaparen av delningen\n {shareUrl} - URL för delningen\n {desc} - Beskrivningen av delningen\n {expires} - Förfallodatumet för delningen\n Variablerna kommer att ersättas med det faktiska värdet.",
|
||||
"admin.config.email.reverse-share-subject": "Omvänd delning ämne",
|
||||
"admin.config.email.reverse-share-subject.description": "Ämne i e-postmeddelandet som skickas när någon skapade en delning med din omvända delningslänk.",
|
||||
"admin.config.email.reverse-share-message": "Omvänd delning meddelande",
|
||||
"admin.config.email.reverse-share-message.description": "Meddelande som skickas när någon skapade en delning med din länk för omvänd delning. {shareUrl} kommer att ersättas med skaparens namn och delningens URL.",
|
||||
"admin.config.email.reset-password-subject": "Återställ lösenord ämne",
|
||||
"admin.config.email.reset-password-subject.description": "Ämne i e-postmeddelandet som skickas när en användare begär en lösenordsåterställning.",
|
||||
"admin.config.email.reset-password-message": "Återställ lösenord meddelande",
|
||||
"admin.config.email.reset-password-message.description": "Meddelande som skickas när en användare begär en lösenordsåterställning. {url} kommer att ersättas med länken för lösenordsåterställningen.",
|
||||
"admin.config.email.invite-subject": "Inbjudan ämne",
|
||||
"admin.config.email.invite-subject.description": "Ämne i e-postmeddelandet som skickas när en administratör bjuder in en användare.",
|
||||
"admin.config.email.invite-message": "Inbjudningsmeddelanden",
|
||||
"admin.config.email.invite-message.description": "Meddelande som skickas när en administratör bjuder in en användare. {url} kommer att ersättas med inbjudningsadressen och {password} med lösenordet.",
|
||||
"admin.config.share.allow-registration": "Tillåt registrering",
|
||||
"admin.config.share.allow-registration.description": "Om registrering är tillåten",
|
||||
"admin.config.share.allow-unauthenticated-shares": "Tillåt oautentiserade delningar",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "Om oautentiserade användare kan skapa delningar",
|
||||
"admin.config.share.max-expiration": "Max utgångsdatum",
|
||||
"admin.config.share.max-expiration.description": "Max längd innan en delning förfaller i timmar. Sätt till 0 för att tillåta obegränsad förfallotid.",
|
||||
"admin.config.share.max-size": "Max storlek",
|
||||
"admin.config.share.max-size.description": "Maximal storlek för delning i bytes",
|
||||
"admin.config.share.zip-compression-level": "Komprimeringsnivå för zip",
|
||||
"admin.config.share.zip-compression-level.description": "Justera nivån för att balansera mellan filstorlek och komprimeringshastighet. Giltiga värden varierar från 0 till 9, med 0 som ingen komprimering och 9 som maximal komprimering. ",
|
||||
"admin.config.smtp.enabled": "Aktiverad",
|
||||
"admin.config.smtp.enabled.description": "Om SMTP skall vara aktiverat. Ange endast detta som sant om du angav adress, port, e-post, användare och lösenord för din SMTP-server.",
|
||||
"admin.config.smtp.host": "Adress",
|
||||
"admin.config.smtp.host.description": "Adress för SMTP-servern",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "Port för SMTP-servern",
|
||||
"admin.config.smtp.email": "E-post",
|
||||
"admin.config.smtp.email.description": "E-postadress som e-postmeddelanden skickas från",
|
||||
"admin.config.smtp.username": "Användarnamn",
|
||||
"admin.config.smtp.username.description": "Användarnamn för SMTP-servern",
|
||||
"admin.config.smtp.password": "Lösenord",
|
||||
"admin.config.smtp.password.description": "Lösenord för SMTP-servern",
|
||||
"admin.config.smtp.button.test": "Skicka testmeddelande",
|
||||
"admin.config.oauth.allow-registration": "Tillåt registrering",
|
||||
"admin.config.oauth.allow-registration.description": "Tillåt användare att registrera sig via social inloggning",
|
||||
"admin.config.oauth.ignore-totp": "Ignorera TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Om du vill ignorera TOTP när användaren använder social inloggning",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Om GitHub-inloggning är aktiverad",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client-ID för GitHub OAuth appen",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret för GitHub OAuth appen",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Om Google-inloggning är aktiverad",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client-ID för Google OAuth appen",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret för Google OAuth appen",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Om Microsoftinloggning skall vara aktiverat",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID för Microsoft OAuth",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret för Microsoft OAuth",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Om inloggning via Discord är aktiverat",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord begränsa till server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Begränsa inloggning till användare på en specifik server. Lämna tomt för att inaktivera.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID för Discord OAuth",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret för Discord OAuth",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "Hoppsan den här sidan finns inte.",
|
||||
"404.button.home": "Ta mig tillbaka hem",
|
||||
// error
|
||||
"error.title": "Fel",
|
||||
"error.description": "Hoppsan!",
|
||||
"error.button.back": "Gå tillbaka",
|
||||
"error.msg.default": "Någonting gick fel.",
|
||||
"error.msg.access_denied": "Du avbröt autentiseringsprocessen, försök igen.",
|
||||
"error.msg.expired_token": "Autentiseringsprocessen tog för lång tid, försök igen.",
|
||||
"error.msg.invalid_token": "Internt fel",
|
||||
"error.msg.no_user": "Användare som är länkad till detta {0} konto finns inte.",
|
||||
"error.msg.no_email": "Kan inte hämta e-postadress från detta {0} konto.",
|
||||
"error.msg.already_linked": "Detta {0} konto är redan länkat till ett annat konto.",
|
||||
"error.msg.not_linked": "Detta {0} konto har ännu inte länkat till något konto.",
|
||||
"error.msg.unverified_account": "Detta {0} -konto är overifierat, försök igen efter verifiering.",
|
||||
"error.msg.discord_guild_permission_denied": "Du är inte tillåten att logga in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "Spara",
|
||||
"common.button.create": "Skapa",
|
||||
"common.button.submit": "Skicka",
|
||||
"common.button.delete": "Ta bort",
|
||||
"common.button.cancel": "Avbryt",
|
||||
"common.button.confirm": "Bekräfta",
|
||||
"common.button.disable": "Inaktivera",
|
||||
"common.button.share": "Delning",
|
||||
"common.button.generate": "Generera",
|
||||
"common.button.done": "Klar",
|
||||
"common.text.link": "Länk",
|
||||
"common.text.or": "eller",
|
||||
"common.button.go-back": "Gå tillbaka",
|
||||
"common.notify.copied": "Din länk har kopierats till urklipp",
|
||||
"common.success": "Slutförd",
|
||||
"common.error": "Fel",
|
||||
"common.error.unknown": "Ett okänt fel har uppstått",
|
||||
"common.error.invalid-email": "Ogiltig e-postadress",
|
||||
"common.error.too-short": "Måste minst vara {length} tecken långt",
|
||||
"common.error.too-long": "Får högst innehålla {length} tecken",
|
||||
"common.error.exact-length": "Måste vara exakt {length} tecken långt",
|
||||
"common.error.invalid-number": "Måste vara ett tal",
|
||||
"common.error.field-required": "Obligatoriskt fält"
|
||||
};
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "เข้าสู่ระบบ",
|
||||
"signIn.notify.totp-required.title": "ยืนยันตรวจสอบสิทธิ์สองปัจจัย",
|
||||
"signIn.notify.totp-required.description": "กรุณาใส่รหัสยืนยันตัวตนสองปัจจัย",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "สมัครบัญชี",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "อีเมล์์์์์์์์ของคุณ",
|
||||
"signup.button.submit": "เริ่มต้นกัน",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Sign in",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "ลืมรหัสผ่าน?",
|
||||
"resetPassword.description": "กรุณาใส่อีเมล์์์์์์์ของคุณเพื่อรีเซ็ตรหัสผ่าน",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "รหัสผ่าน",
|
||||
"account.card.password.old": "รหัสผ่านเก่า",
|
||||
"account.card.password.new": "รหัสผ่านใหม่",
|
||||
"account.card.password.noPasswordSet": "You don't have a password set. If you want to sign in with email and password you need to set a password.",
|
||||
"account.notify.password.success": "อัปเดตรหัสผ่านเรียบร้อยแล้ว",
|
||||
"account.card.oauth.title": "Social login",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description": "Unlinking your social accounts may cause you to lose your account if you don't remember your username and password.",
|
||||
"account.notify.oauth.unlinked.success": "Unlinked successfully",
|
||||
"account.card.security.title": "ความปลอดภัย",
|
||||
"account.card.security.totp.enable.description": "ใส่รหัสผ่านปัจจุบันของคุณเพื่อเริ่มต้นการเปิดใช้งาน TOTP",
|
||||
"account.card.security.totp.disable.description": "ใส่รหัสผ่านปัจจุบันของคุณเพื่อปิดใช้งาน TOTP",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"upload.modal.not-signed-in-description": "คุณจะไม่สามารถลบการแชร์ของคุณด้วยตนเองและดูจำนวนผู้เข้าชมได้",
|
||||
"upload.modal.expires.never": "ไม่มีกำหนด",
|
||||
"upload.modal.expires.never-long": "ไม่มีกำหนดหมดอายุ",
|
||||
"upload.modal.expires.error.too-long": "Expiration exceeds maximum expiration date of {max}.",
|
||||
"upload.modal.link.label": "ลิงค์",
|
||||
"upload.modal.expires.label": "หมดอายุ",
|
||||
"upload.modal.expires.minute-singular": "นาที",
|
||||
@@ -240,12 +264,19 @@ export default {
|
||||
"share.modal.file-preview.error.not-supported.title": "ไม่รองรับการแสดงตัวอย่าง",
|
||||
"share.modal.file-preview.error.not-supported.description": "ไม่รองรับการแสดงตัวอย่างสำหรับไฟล์ประเภทนี้ โปรดดาวน์โหลดไฟล์เพื่อดู",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "Edit {shareId}",
|
||||
"share.edit.append-upload": "Append file",
|
||||
"share.edit.notify.generic-error": "An error occurred while finishing your share.",
|
||||
"share.edit.notify.save-success": "Share updated successfully",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "การตั้งค่า",
|
||||
"admin.config.category.general": "ทั่วไป",
|
||||
"admin.config.category.share": "การแชร์",
|
||||
"admin.config.category.email": "อีเมล์์์์์์",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "Social Login",
|
||||
"admin.config.general.app-name": "ชื่อแอพ",
|
||||
"admin.config.general.app-name.description": "ชื่อแอพพลิเคชัน",
|
||||
"admin.config.general.app-url": "URL ของแอพ",
|
||||
@@ -277,6 +308,8 @@ export default {
|
||||
"admin.config.share.allow-registration.description": "อนุญาตให้ผู้ใช้ลงทะเบียนด้วยตัวเองเพื่อสร้างแชร์",
|
||||
"admin.config.share.allow-unauthenticated-shares": "อนุญาตให้แชร์โดยไม่ต้องเข้าสู่ระบบ",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "อนุญาตให้ผู้ใช้ที่ไม่ได้เข้าสู่ระบบสร้างแชร์",
|
||||
"admin.config.share.max-expiration": "Max expiration",
|
||||
"admin.config.share.max-expiration.description": "Maximum share expiration in hours. Set to 0 to allow unlimited expiration.",
|
||||
"admin.config.share.max-size": "ขนาดสูงสุด",
|
||||
"admin.config.share.max-size.description": "ขนาดสูงสุดของแชร์",
|
||||
"admin.config.share.zip-compression-level": "ระดับการบีบอัดไฟล์ Zip",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "รหัสผ่าน",
|
||||
"admin.config.smtp.password.description": "รหัสผ่านของเซิร์ฟเวอร์ SMTP",
|
||||
"admin.config.smtp.button.test": "ส่งอีเมล์์์์์์ทดสอบ",
|
||||
"admin.config.oauth.allow-registration": "Allow registration",
|
||||
"admin.config.oauth.allow-registration.description": "Allow users to register via social login",
|
||||
"admin.config.oauth.ignore-totp": "Ignore TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "Whether to ignore TOTP when user using social login",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "Whether GitHub login is enabled",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "Client ID of the GitHub OAuth app",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "Client secret of the GitHub OAuth app",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "Whether Google login is enabled",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Client ID of the Google OAuth app",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Client secret of the Google OAuth app",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "Whether Microsoft login is enabled",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Client ID of the Microsoft OAuth app",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Client secret of the Microsoft OAuth app",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Whether Discord login is enabled",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Client ID of the Discord OAuth app",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Client secret of the Discord OAuth app",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "ไม่พบหน้าที่คุณกำลังมองหา",
|
||||
"404.button.home": "หน้าแรก",
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "Go back",
|
||||
"error.msg.default": "Something went wrong.",
|
||||
"error.msg.access_denied": "You canceled the authentication process, please try again.",
|
||||
"error.msg.expired_token": "The authentication process took too long, please try again.",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "User linked to this {0} account doesn't exist.",
|
||||
"error.msg.no_email": "Can't get email address from this {0} account.",
|
||||
"error.msg.already_linked": "This {0} account is already linked to another account.",
|
||||
"error.msg.not_linked": "This {0} account haven't linked to any account yet.",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "บันทึก",
|
||||
"common.button.create": "สร้าง",
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
"signin.button.submit": "登录",
|
||||
"signIn.notify.totp-required.title": "请继续两步验证",
|
||||
"signIn.notify.totp-required.description": "请输入一次性验证码",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "谷歌",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// END /auth/signin
|
||||
// /auth/signup
|
||||
"signup.title": "创建账户",
|
||||
@@ -44,6 +50,10 @@ export default {
|
||||
"signup.input.email.placeholder": "请输入电子邮件",
|
||||
"signup.button.submit": "注册",
|
||||
// END /auth/signup
|
||||
// /auth/totp
|
||||
"totp.title": "两步验证",
|
||||
"totp.button.signIn": "登录",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "忘记密码?",
|
||||
"resetPassword.description": "请输入电子邮件接受重置密码邮件",
|
||||
@@ -62,7 +72,20 @@ export default {
|
||||
"account.card.password.title": "密码",
|
||||
"account.card.password.old": "旧密码",
|
||||
"account.card.password.new": "新密码",
|
||||
"account.card.password.noPasswordSet": "您还未设置密码。如果您想通过邮箱和密码登录,请先设置密码。",
|
||||
"account.notify.password.success": "密码更改成功!",
|
||||
"account.card.oauth.title": "社交账号登录",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "谷歌",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "Link",
|
||||
"account.card.oauth.unlink": "Unlink",
|
||||
"account.card.oauth.unlinked": "Unlinked",
|
||||
"account.modal.unlink.title": "Unlink account",
|
||||
"account.modal.unlink.description": "如果您不记得您的用户名和密码,解除和社交账号的关联可能会导致丢失账户。",
|
||||
"account.notify.oauth.unlinked.success": "解除关联成功",
|
||||
"account.card.security.title": "安全",
|
||||
"account.card.security.totp.enable.description": "请输入当前密码开启两步验证",
|
||||
"account.card.security.totp.disable.description": "请输入当前密码关闭两步验证",
|
||||
@@ -77,7 +100,7 @@ export default {
|
||||
"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.language.description": "本项目由社区成员翻译,部分语言可能暂未提供翻译。",
|
||||
"account.card.color.title": "颜色外观",
|
||||
// ThemeSwitcher.tsx
|
||||
"account.theme.dark": "暗黑模式",
|
||||
@@ -107,11 +130,11 @@ export default {
|
||||
// END /account/shares
|
||||
// /account/reverseShares
|
||||
"account.reverseShares.title": "预留共享",
|
||||
"account.reverseShares.description": "预留共享允许你创建一个特定共享链接,以便外部用户以此创建共享",
|
||||
"account.reverseShares.description": "预留共享允许你创建一个特定共享链接,以便外部用户以此链接创建共享",
|
||||
"account.reverseShares.title.empty": "这里空空如也 👀",
|
||||
"account.reverseShares.description.empty": "你没有创建任何预留共享",
|
||||
// showCreateReverseShareModal.tsx
|
||||
"account.reverseShares.modal.title": "Create reverse share",
|
||||
"account.reverseShares.modal.title": "创建反向共享",
|
||||
"account.reverseShares.modal.expiration.label": "过期时间",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "1 分钟",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "分钟",
|
||||
@@ -191,6 +214,7 @@ export default {
|
||||
"upload.modal.not-signed-in-description": "你将不能删除你的共享或查看访问次数",
|
||||
"upload.modal.expires.never": "永不",
|
||||
"upload.modal.expires.never-long": "永不过期",
|
||||
"upload.modal.expires.error.too-long": "您设置的过期时间超出了系统上限 {max}。",
|
||||
"upload.modal.link.label": "共享链接",
|
||||
"upload.modal.expires.label": "过期时间",
|
||||
"upload.modal.expires.minute-singular": "1 分钟",
|
||||
@@ -240,27 +264,34 @@ export default {
|
||||
"share.modal.file-preview.error.not-supported.title": "该文件类型不支持预览",
|
||||
"share.modal.file-preview.error.not-supported.description": "该文件类型不支持预览,请下载后打开查看",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "编辑 {shareId}",
|
||||
"share.edit.append-upload": "追加文件",
|
||||
"share.edit.notify.generic-error": "保存共享的过程中发生了错误",
|
||||
"share.edit.notify.save-success": "共享已更新成功",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "配置管理",
|
||||
"admin.config.category.general": "通用",
|
||||
"admin.config.category.share": "共享",
|
||||
"admin.config.category.email": "电子邮件",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "社交账号登录",
|
||||
"admin.config.general.app-name": "App 名称",
|
||||
"admin.config.general.app-name.description": "这个 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.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.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": "预留共享邮件通知内容",
|
||||
@@ -277,10 +308,12 @@ export default {
|
||||
"admin.config.share.allow-registration.description": "是否允许注册",
|
||||
"admin.config.share.allow-unauthenticated-shares": "是否允许未验证的共享",
|
||||
"admin.config.share.allow-unauthenticated-shares.description": "是否允许未验证的用户创建共享",
|
||||
"admin.config.share.max-expiration": "最长过期时间",
|
||||
"admin.config.share.max-expiration.description": "“最长过期时间”以小时为单位,如果将其设置为“0”则为永不过期。",
|
||||
"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.share.zip-compression-level": "Zip 文件压缩质量",
|
||||
"admin.config.share.zip-compression-level.description": "调整压缩质量来平衡压缩文件的大小和压缩的速度。有效值介于 0 和 9 之间,0 为不压缩,9 为最高质量压缩。 ",
|
||||
"admin.config.smtp.enabled": "启用",
|
||||
"admin.config.smtp.enabled.description": "是否开启 SMTP,仅当输入主机名、端口、发送邮箱、用户名和密码后开启",
|
||||
"admin.config.smtp.host": "主机名",
|
||||
@@ -294,9 +327,71 @@ export default {
|
||||
"admin.config.smtp.password": "密码",
|
||||
"admin.config.smtp.password.description": "SMTP 主机密码",
|
||||
"admin.config.smtp.button.test": "发送测试邮件",
|
||||
"admin.config.oauth.allow-registration": "允许注册",
|
||||
"admin.config.oauth.allow-registration.description": "允许用户通过登录社交账号来注册",
|
||||
"admin.config.oauth.ignore-totp": "忽略两步验证",
|
||||
"admin.config.oauth.ignore-totp.description": "用户通过社交账号登录时是否忽略两步验证",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "是否启用 GitHub 账号登录",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "GitHub OAuth App 的 Client ID",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "GitHub OAuth App 的 Client secret",
|
||||
"admin.config.oauth.google-enabled": "谷歌",
|
||||
"admin.config.oauth.google-enabled.description": "是否启用谷歌账号登录",
|
||||
"admin.config.oauth.google-client-id": "谷歌 Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Google OAuth App 的 Client ID",
|
||||
"admin.config.oauth.google-client-secret": "谷歌 Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Google OAuth App 的 Client secret",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "是否启用微软账号登录",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Tenant ID of the Microsoft OAuth app\ncommon: Users with both a personal Microsoft account and a work or school account from Microsoft Entra ID can sign in to the application. organizations: Only users with work or school accounts from Microsoft Entra ID can sign in to the application.\nconsumers: Only users with a personal Microsoft account can sign in to the application.\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: Only users from a specific Microsoft Entra tenant (directory members with a work or school account or directory guests with a personal Microsoft account) can sign in to the application.",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Microsoft OAuth App 的 Client ID",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft 的 Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Microsoft OAuth App 的 Client secret",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "是否启用 Discord 账号登录",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord 的 limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "限制特定服务器中的用户登录。留空来禁用。",
|
||||
"admin.config.oauth.discord-client-id": "Discord 的 Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Discord OAuth App 的 Client ID",
|
||||
"admin.config.oauth.discord-client-secret": "Discord 的 Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Discord OAuth App 的 Client secret",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "是否启用 OpenID Connect 登录",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect 的 Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "OpenID Connect OAuth App 的 Discovery URI",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect 用户名请求",
|
||||
"admin.config.oauth.oidc-username-claim.description": "OpenID Connect ID token 中的用户名请求。如果您不知道这项配置是什么,请留空。",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect 的 Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "OpenID Connect OAuth App 的 Client ID",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect 的 Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "OpenID Connect OAuth App 的 Client secret",
|
||||
// 404
|
||||
"404.description": "当前的页面走丢啦",
|
||||
"404.button.home": "返回主页",
|
||||
// error
|
||||
"error.title": "出错了",
|
||||
"error.description": "糟糕!",
|
||||
"error.button.back": "返回上一页",
|
||||
"error.msg.default": "发生未知错误。",
|
||||
"error.msg.access_denied": "您取消了认证过程,请重试。",
|
||||
"error.msg.expired_token": "认证过程耗时异常,请重试。",
|
||||
"error.msg.invalid_token": "内部错误",
|
||||
"error.msg.no_user": "用户关联的这个 {0} 账户不存在。",
|
||||
"error.msg.no_email": "无法从 {0} 这个账户获取邮箱地址。",
|
||||
"error.msg.already_linked": "{0} 这个账户已经关联到另一个账号。",
|
||||
"error.msg.not_linked": "{0} 这个账户尚未关联到任何账号。",
|
||||
"error.msg.unverified_account": "{0} 这个账户尚未验证,请在验证后重试。",
|
||||
"error.msg.discord_guild_permission_denied": "您无权登录。",
|
||||
"error.msg.cannot_get_user_info": "无法从 {0} 这个账户获取您的用户信息。",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "谷歌",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// Common translations
|
||||
"common.button.save": "保存",
|
||||
"common.button.create": "创建",
|
||||
@@ -309,10 +404,11 @@ export default {
|
||||
"common.button.generate": "生成",
|
||||
"common.button.done": "完成",
|
||||
"common.text.link": "链接",
|
||||
"common.text.navigate-to-link": "访问链接",
|
||||
"common.text.or": "或",
|
||||
"common.button.go-back": "返回",
|
||||
"common.notify.copied": "已复制到剪贴板",
|
||||
"common.success": "Success",
|
||||
"common.success": "成功",
|
||||
"common.error": "错误",
|
||||
"common.error.unknown": "发生未知错误",
|
||||
"common.error.invalid-email": "邮件地址不可用",
|
||||
|
||||
419
frontend/src/i18n/translations/zh-TW.ts
Normal file
419
frontend/src/i18n/translations/zh-TW.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
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": "請輸入一次性驗證碼",
|
||||
"signIn.oauth.or": "OR",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
"signIn.oauth.discord": "Discord",
|
||||
"signIn.oauth.oidc": "OpenID",
|
||||
// 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/totp
|
||||
"totp.title": "TOTP Authentication",
|
||||
"totp.button.signIn": "Sign in",
|
||||
// END /auth/totp
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "忘記密碼?",
|
||||
"resetPassword.description": "請輸入Email以讓系統寄送重置密碼郵件",
|
||||
"resetPassword.notify.success": "一封包含密碼重置地址的郵件已發送到您的Email",
|
||||
"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": "Email",
|
||||
"account.notify.info.success": "帳號資訊更新成功!",
|
||||
"account.card.password.title": "密碼",
|
||||
"account.card.password.old": "舊密碼",
|
||||
"account.card.password.new": "新密碼",
|
||||
"account.card.password.noPasswordSet": "並未設定密碼,若希望使用E-mail與密碼進行登入則必須要設定密碼。",
|
||||
"account.notify.password.success": "密碼更改成功!",
|
||||
"account.card.oauth.title": "第三方登入",
|
||||
"account.card.oauth.github": "GitHub",
|
||||
"account.card.oauth.google": "Google",
|
||||
"account.card.oauth.microsoft": "Microsoft",
|
||||
"account.card.oauth.discord": "Discord",
|
||||
"account.card.oauth.oidc": "OpenID",
|
||||
"account.card.oauth.link": "聯結",
|
||||
"account.card.oauth.unlink": "取消聯結",
|
||||
"account.card.oauth.unlinked": "已取消聯結",
|
||||
"account.modal.unlink.title": "取消聯結第三方登入",
|
||||
"account.modal.unlink.description": "若不記得使用者名稱與密碼,取消聯結第三方登入可能會使您無法登入。",
|
||||
"account.notify.oauth.unlinked.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": "此專案由社群進行翻譯,部分語言可能尚未完成翻譯。",
|
||||
"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.title": "建立檔案請求",
|
||||
"account.reverseShares.modal.expiration.label": "過期時間",
|
||||
"account.reverseShares.modal.expiration.minute-singular": "分鐘",
|
||||
"account.reverseShares.modal.expiration.minute-plural": "分鐘",
|
||||
"account.reverseShares.modal.expiration.hour-singular": "小時",
|
||||
"account.reverseShares.modal.expiration.hour-plural": "小時",
|
||||
"account.reverseShares.modal.expiration.day-singular": "天",
|
||||
"account.reverseShares.modal.expiration.day-plural": "天",
|
||||
"account.reverseShares.modal.expiration.week-singular": "周",
|
||||
"account.reverseShares.modal.expiration.week-plural": "周",
|
||||
"account.reverseShares.modal.expiration.month-singular": "月",
|
||||
"account.reverseShares.modal.expiration.month-plural": "月",
|
||||
"account.reverseShares.modal.expiration.year-singular": "年",
|
||||
"account.reverseShares.modal.expiration.year-plural": "年",
|
||||
"account.reverseShares.modal.max-size.label": "上傳大小上限",
|
||||
"account.reverseShares.modal.send-email": "發送Email提醒",
|
||||
"account.reverseShares.modal.send-email.description": "當這個檔案請求聯結被用於分享時,發送Email提醒",
|
||||
"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": "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": "Email",
|
||||
"admin.users.modal.create.password": "密碼",
|
||||
"admin.users.modal.create.manual-password": "手動設置密碼",
|
||||
"admin.users.modal.create.manual-password.description": "如果不勾選,使用者將會收到一封Email來設置他們的密碼",
|
||||
"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.expires.error.too-long": "過期時間超出了系統上限設定 {max}。",
|
||||
"upload.modal.link.label": "分享聯結",
|
||||
"upload.modal.expires.label": "過期時間",
|
||||
"upload.modal.expires.minute-singular": "分鐘",
|
||||
"upload.modal.expires.minute-plural": "分鐘",
|
||||
"upload.modal.expires.hour-singular": "小時",
|
||||
"upload.modal.expires.hour-plural": "小時",
|
||||
"upload.modal.expires.day-singular": "天",
|
||||
"upload.modal.expires.day-plural": "天",
|
||||
"upload.modal.expires.week-singular": "周",
|
||||
"upload.modal.expires.week-plural": "周",
|
||||
"upload.modal.expires.month-singular": "月",
|
||||
"upload.modal.expires.month-plural": "月",
|
||||
"upload.modal.expires.year-singular": "年",
|
||||
"upload.modal.expires.year-plural": "年",
|
||||
"upload.modal.accordion.description.title": "描述",
|
||||
"upload.modal.accordion.description.placeholder": "分享檔案備註資訊",
|
||||
"upload.modal.accordion.email.title": "Email提醒",
|
||||
"upload.modal.accordion.email.placeholder": "收件人Email地址",
|
||||
"upload.modal.accordion.email.invalid-email": "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]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "編輯 {shareId}",
|
||||
"share.edit.append-upload": "追加檔案",
|
||||
"share.edit.notify.generic-error": "保存分享的過程中發生了錯誤",
|
||||
"share.edit.notify.save-success": "分享已更新成功",
|
||||
// END /share/[id]/edit
|
||||
// /admin/config
|
||||
"admin.config.title": "配置管理",
|
||||
"admin.config.category.general": "通用",
|
||||
"admin.config.category.share": "分享",
|
||||
"admin.config.category.email": "Email",
|
||||
"admin.config.category.smtp": "SMTP",
|
||||
"admin.config.category.oauth": "第三方登入",
|
||||
"admin.config.general.app-name": "App 名稱",
|
||||
"admin.config.general.app-name.description": "這個 App 的名稱",
|
||||
"admin.config.general.app-url": "App URL",
|
||||
"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": "開啟分享Email通知",
|
||||
"admin.config.email.enable-share-email-recipients.description": "是否允許通過Email通知發送分享資訊,需進行 SMTP 設定後此選項才有效",
|
||||
"admin.config.email.share-recipients-subject": "分享Email通知主題",
|
||||
"admin.config.email.share-recipients-subject.description": "發送分享Email通知的主題",
|
||||
"admin.config.email.share-recipients-message": "分享Email通知內容",
|
||||
"admin.config.email.share-recipients-message.description": "發送到接收者的分享Email通知具體內容。可使用的參數有:\n {creator} - 分享建立者\n {shareUrl} - 分享聯結\n {desc} - 分享描述\n {expires} - 分享過期時間\n 這些參數會被實際的值所替代",
|
||||
"admin.config.email.reverse-share-subject": "檔案請求Email通知主題",
|
||||
"admin.config.email.reverse-share-subject.description": "當有人使用了您的檔案請求聯結時,發送的檔案請求通知主題",
|
||||
"admin.config.email.reverse-share-message": "檔案請求Email通知內容",
|
||||
"admin.config.email.reverse-share-message.description": "當有人使用了您的檔案請求聯結時,發送的通知內容。{shareUrl} 會被建立者的使用者名稱和分享聯結代替",
|
||||
"admin.config.email.reset-password-subject": "重置密碼Email通知主題",
|
||||
"admin.config.email.reset-password-subject.description": "當使用者發動重置密碼時,發送的重置密碼通知主題",
|
||||
"admin.config.email.reset-password-message": "重置密碼Email通知內容",
|
||||
"admin.config.email.reset-password-message.description": "當使用者發動重置密碼時,重置密碼通知內容。{url} 會被重置密碼聯結代替",
|
||||
"admin.config.email.invite-subject": "邀請Email通知主題",
|
||||
"admin.config.email.invite-subject.description": "當管理員邀請使用者時,發送的邀請通知主題",
|
||||
"admin.config.email.invite-message": "邀請Email通知內容",
|
||||
"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-expiration": "最大過期時間",
|
||||
"admin.config.share.max-expiration.description": "最大過期時間(小時),若設定為0則為永不過期",
|
||||
"admin.config.share.max-size": "最大檔案上限",
|
||||
"admin.config.share.max-size.description": "最大檔案上限,單位 bytes (1GB=1024MB=1048576KB=1073741824bytes)",
|
||||
"admin.config.share.zip-compression-level": "Zip 壓縮等級",
|
||||
"admin.config.share.zip-compression-level.description": "調整壓縮等級以平衡檔案大小和壓縮速度。 有效值範圍從 0 到 9,其中 0 表示無壓縮,9 表示最大壓縮",
|
||||
"admin.config.smtp.enabled": "啟用",
|
||||
"admin.config.smtp.enabled.description": "是否開啟 SMTP,需輸入Host、Port、發送郵箱、使用者名稱和密碼後才有作用",
|
||||
"admin.config.smtp.host": "Host",
|
||||
"admin.config.smtp.host.description": "SMTP Host",
|
||||
"admin.config.smtp.port": "Port",
|
||||
"admin.config.smtp.port.description": "SMTP Port",
|
||||
"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": "發送測試Email",
|
||||
"admin.config.oauth.allow-registration": "允許註冊",
|
||||
"admin.config.oauth.allow-registration.description": "允許使用者以第三方登入註冊",
|
||||
"admin.config.oauth.ignore-totp": "略過 TOTP",
|
||||
"admin.config.oauth.ignore-totp.description": "當使用者使用第三方登入時,略過 TOTP 驗證",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "啟用 Github 登入",
|
||||
"admin.config.oauth.github-client-id": "GitHub Client ID",
|
||||
"admin.config.oauth.github-client-id.description": "GitHub OAuth 的 Client ID",
|
||||
"admin.config.oauth.github-client-secret": "GitHub Client secret",
|
||||
"admin.config.oauth.github-client-secret.description": "GitHub OAuth 的 Client secret",
|
||||
"admin.config.oauth.google-enabled": "Google",
|
||||
"admin.config.oauth.google-enabled.description": "啟用 Google 登入",
|
||||
"admin.config.oauth.google-client-id": "Google Client ID",
|
||||
"admin.config.oauth.google-client-id.description": "Google OAuth 的 Client ID",
|
||||
"admin.config.oauth.google-client-secret": "Google Client secret",
|
||||
"admin.config.oauth.google-client-secret.description": "Google OAuth 的 Client secret",
|
||||
"admin.config.oauth.microsoft-enabled": "Microsoft",
|
||||
"admin.config.oauth.microsoft-enabled.description": "啟用 Microsoft 登入",
|
||||
"admin.config.oauth.microsoft-tenant": "Microsoft Tenant",
|
||||
"admin.config.oauth.microsoft-tenant.description": "Microsoft OAuth 的 Tenant ID\ncommon: 擁有 Microsoft Entra ID 的個人 Microsoft 帳號和工作或學校帳號的使用者可以登入該應用程式。\norganizations: 擁有 Microsoft Entra ID 的工作或學校帳號的使用者才能登入應用程式。\nconsumers: 只有擁有個人 Microsoft 帳號的使用者才能登入該應用程式。\ndomain name of the Microsoft Entra tenant or the tenant ID in GUID format: 只有來自特定 Microsoft Entra 租戶的使用者(具有工作或學校帳號的目錄成員或具有個人 Microsoft 帳號的目錄成員)才能登入應用程式。",
|
||||
"admin.config.oauth.microsoft-client-id": "Microsoft Client ID",
|
||||
"admin.config.oauth.microsoft-client-id.description": "Microsoft OAuth 的 Client ID",
|
||||
"admin.config.oauth.microsoft-client-secret": "Microsoft Client secret",
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Microsoft OAuth 的 Client secret",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "啟用 Discord 登入",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-client-id": "Discord Client ID",
|
||||
"admin.config.oauth.discord-client-id.description": "Discord OAuth 的 Client ID",
|
||||
"admin.config.oauth.discord-client-secret": "Discord Client secret",
|
||||
"admin.config.oauth.discord-client-secret.description": "Discord OAuth 的 Client secret",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
// 404
|
||||
"404.description": "查無此頁",
|
||||
"404.button.home": "返回主頁",
|
||||
// error
|
||||
"error.title": "Error",
|
||||
"error.description": "Oops!",
|
||||
"error.button.back": "返回上一頁",
|
||||
"error.msg.default": "發生預期外的問題。",
|
||||
"error.msg.access_denied": "您取消了身分驗證,請重試。",
|
||||
"error.msg.expired_token": "身分驗證過程逾時,請重試。",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.no_user": "與此 {0} 帳號關聯的使用者不存在。",
|
||||
"error.msg.no_email": "無法從此 {0} 帳號取得Email地址。",
|
||||
"error.msg.already_linked": "此 {0} 帳號已與另一個帳號關聯。",
|
||||
"error.msg.not_linked": "此 {0} 帳號尚未關聯到任何帳號。",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.discord_guild_permission_denied": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
"error.param.provider_discord": "Discord",
|
||||
"error.param.provider_oidc": "OpenID Connect",
|
||||
// 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": "成功",
|
||||
"common.error": "錯誤",
|
||||
"common.error.unknown": "發生預期外錯誤",
|
||||
"common.error.invalid-email": "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/*", "/s/*", "/upload/*"]),
|
||||
public: new Routes(["/share/*", "/s/*", "/upload/*", "/error"]),
|
||||
admin: new Routes(["/admin/*"]),
|
||||
account: new Routes(["/account*"]),
|
||||
disabled: new Routes([]),
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { useForm, yupResolver } from "@mantine/form";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tb2Fa } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
@@ -20,16 +21,28 @@ import Meta from "../../components/Meta";
|
||||
import LanguagePicker from "../../components/account/LanguagePicker";
|
||||
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
|
||||
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
|
||||
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 userService from "../../services/user.service";
|
||||
import { getOAuthIcon, getOAuthUrl, unlinkOAuth } from "../../utils/oauth.util";
|
||||
import toast from "../../utils/toast.util";
|
||||
|
||||
const Account = () => {
|
||||
const [oauth, setOAuth] = useState<string[]>([]);
|
||||
const [oauthStatus, setOAuthStatus] = useState<Record<
|
||||
string,
|
||||
{
|
||||
provider: string;
|
||||
providerUsername: string;
|
||||
}
|
||||
> | null>(null);
|
||||
|
||||
const { user, refreshUser } = useUser();
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
const config = useConfig();
|
||||
|
||||
const accountForm = useForm({
|
||||
initialValues: {
|
||||
@@ -53,10 +66,14 @@ const Account = () => {
|
||||
},
|
||||
validate: yupResolver(
|
||||
yup.object().shape({
|
||||
oldPassword: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
oldPassword: yup.string().when([], {
|
||||
is: () => !!user?.hasPassword,
|
||||
then: (schema) =>
|
||||
schema
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
.required(t("common.error.field-required")),
|
||||
otherwise: (schema) => schema.notRequired(),
|
||||
}),
|
||||
password: yup
|
||||
.string()
|
||||
.min(8, t("common.error.too-short", { length: 8 }))
|
||||
@@ -96,6 +113,25 @@ const Account = () => {
|
||||
),
|
||||
});
|
||||
|
||||
const refreshOAuthStatus = () => {
|
||||
authService
|
||||
.getOAuthStatus()
|
||||
.then((data) => {
|
||||
setOAuthStatus(data.data);
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
authService
|
||||
.getAvailableOAuth()
|
||||
.then((data) => {
|
||||
setOAuth(data.data);
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
refreshOAuthStatus();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("account.title")} />
|
||||
@@ -143,7 +179,8 @@ const Account = () => {
|
||||
onSubmit={passwordForm.onSubmit((values) =>
|
||||
authService
|
||||
.updatePassword(values.oldPassword, values.password)
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
refreshUser();
|
||||
toast.success(t("account.notify.password.success"));
|
||||
passwordForm.reset();
|
||||
})
|
||||
@@ -151,10 +188,16 @@ const Account = () => {
|
||||
)}
|
||||
>
|
||||
<Stack>
|
||||
<PasswordInput
|
||||
label={t("account.card.password.old")}
|
||||
{...passwordForm.getInputProps("oldPassword")}
|
||||
/>
|
||||
{user?.hasPassword ? (
|
||||
<PasswordInput
|
||||
label={t("account.card.password.old")}
|
||||
{...passwordForm.getInputProps("oldPassword")}
|
||||
/>
|
||||
) : (
|
||||
<Text size="sm" color="dimmed">
|
||||
<FormattedMessage id="account.card.password.noPasswordSet" />
|
||||
</Text>
|
||||
)}
|
||||
<PasswordInput
|
||||
label={t("account.card.password.new")}
|
||||
{...passwordForm.getInputProps("password")}
|
||||
@@ -167,7 +210,79 @@ const Account = () => {
|
||||
</Stack>
|
||||
</form>
|
||||
</Paper>
|
||||
{oauth.length > 0 && (
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
<FormattedMessage id="account.card.oauth.title" />
|
||||
</Title>
|
||||
|
||||
<Tabs defaultValue={oauth[0] || ""}>
|
||||
<Tabs.List>
|
||||
{oauth.map((provider) => (
|
||||
<Tabs.Tab
|
||||
value={provider}
|
||||
icon={getOAuthIcon(provider)}
|
||||
key={provider}
|
||||
>
|
||||
{t(`account.card.oauth.${provider}`)}
|
||||
</Tabs.Tab>
|
||||
))}
|
||||
</Tabs.List>
|
||||
{oauth.map((provider) => (
|
||||
<Tabs.Panel value={provider} pt="xs" key={provider}>
|
||||
<Group position="apart">
|
||||
<Text>
|
||||
{oauthStatus?.[provider]
|
||||
? oauthStatus[provider].providerUsername
|
||||
: t("account.card.oauth.unlinked")}
|
||||
</Text>
|
||||
{oauthStatus?.[provider] ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: t("account.modal.unlink.title"),
|
||||
children: (
|
||||
<Text>
|
||||
{t("account.modal.unlink.description")}
|
||||
</Text>
|
||||
),
|
||||
labels: {
|
||||
confirm: t("account.card.oauth.unlink"),
|
||||
cancel: t("common.button.cancel"),
|
||||
},
|
||||
confirmProps: { color: "red" },
|
||||
onConfirm: () => {
|
||||
unlinkOAuth(provider)
|
||||
.then(() => {
|
||||
toast.success(
|
||||
t("account.notify.oauth.unlinked.success"),
|
||||
);
|
||||
refreshOAuthStatus();
|
||||
})
|
||||
.catch(toast.axiosError);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("account.card.oauth.unlink")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
component="a"
|
||||
href={getOAuthUrl(
|
||||
config.get("general.appUrl"),
|
||||
provider,
|
||||
)}
|
||||
>
|
||||
{t("account.card.oauth.link")}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Tabs.Panel>
|
||||
))}
|
||||
</Tabs>
|
||||
</Paper>
|
||||
)}
|
||||
<Paper withBorder p="xl" mt="lg">
|
||||
<Title order={5} mb="xs">
|
||||
<FormattedMessage id="account.card.security.title" />
|
||||
|
||||
@@ -77,6 +77,7 @@ const MyShares = () => {
|
||||
showCreateReverseShareModal(
|
||||
modals,
|
||||
config.get("smtp.enabled"),
|
||||
config.get("share.maxExpiration"),
|
||||
getReverseShares,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useModals } from "@mantine/modals";
|
||||
import moment from "moment";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { TbEdit, TbInfoCircle, TbLink, TbTrash } from "react-icons/tb";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Meta from "../../components/Meta";
|
||||
import showShareInformationsModal from "../../components/account/showShareInformationsModal";
|
||||
@@ -110,6 +110,11 @@ const MyShares = () => {
|
||||
</td>
|
||||
<td>
|
||||
<Group position="right">
|
||||
<Link href={`/share/${share.id}/edit`}>
|
||||
<ActionIcon color="orange" variant="light" size={25}>
|
||||
<TbEdit />
|
||||
</ActionIcon>
|
||||
</Link>
|
||||
<ActionIcon
|
||||
color="blue"
|
||||
variant="light"
|
||||
|
||||
@@ -24,10 +24,7 @@ import CenterLoader from "../../../components/core/CenterLoader";
|
||||
import useConfig from "../../../hooks/config.hook";
|
||||
import configService from "../../../services/config.service";
|
||||
import { AdminConfig, UpdateConfig } from "../../../types/config.type";
|
||||
import {
|
||||
camelToKebab,
|
||||
capitalizeFirstLetter,
|
||||
} from "../../../utils/string.util";
|
||||
import { camelToKebab } from "../../../utils/string.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
|
||||
@@ -128,7 +125,7 @@ export default function AppShellDemo() {
|
||||
<>
|
||||
<Stack>
|
||||
<Title mb="md" order={3}>
|
||||
{capitalizeFirstLetter(categoryId)}
|
||||
{t("admin.config.category." + categoryId)}
|
||||
</Title>
|
||||
{configVariables.map((configVariable) => (
|
||||
<Group key={configVariable.key} position="apart">
|
||||
|
||||
@@ -68,13 +68,13 @@ const ResetPassword = () => {
|
||||
})}
|
||||
>
|
||||
<PasswordInput
|
||||
label={t("resetPassword.text.password")}
|
||||
label={t("resetPassword.input.password")}
|
||||
placeholder="••••••••••"
|
||||
{...form.getInputProps("password")}
|
||||
/>
|
||||
<Group position="right" mt="lg">
|
||||
<Button type="submit" className={classes.control}>
|
||||
<FormattedMessage id="resetPassword.button.resetPassword" />
|
||||
<FormattedMessage id="resetPassword.text.resetPassword" />
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
18
frontend/src/pages/auth/totp/[loginToken].tsx
Normal file
18
frontend/src/pages/auth/totp/[loginToken].tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import Meta from "../../../components/Meta";
|
||||
import TotpForm from "../../../components/auth/TotpForm";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
const Totp = () => {
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("totp.title")} />
|
||||
<TotpForm redirectPath={(router.query.redirect as string) || "/upload"} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Totp;
|
||||
49
frontend/src/pages/error.tsx
Normal file
49
frontend/src/pages/error.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import { Button, createStyles, Stack, Text, Title } from "@mantine/core";
|
||||
import Meta from "../components/Meta";
|
||||
import useTranslate from "../hooks/useTranslate.hook";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
const useStyle = createStyles({
|
||||
title: {
|
||||
fontSize: 100,
|
||||
},
|
||||
});
|
||||
|
||||
export default function Error() {
|
||||
const { classes } = useStyle();
|
||||
const t = useTranslate();
|
||||
const router = useRouter();
|
||||
|
||||
const params = router.query.params
|
||||
? (router.query.params as string).split(",").map((param) => {
|
||||
return t(`error.param.${param}`);
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("error.title")} />
|
||||
<Stack align="center">
|
||||
<Title order={3} className={classes.title}>
|
||||
{t("error.description")}
|
||||
</Title>
|
||||
<Text mt="xl" size="lg">
|
||||
<FormattedMessage
|
||||
id={`error.msg.${router.query.error || "default"}`}
|
||||
values={Object.fromEntries(
|
||||
[params].map((value, key) => [key.toString(), value]),
|
||||
)}
|
||||
/>
|
||||
</Text>
|
||||
<Button
|
||||
mt="xl"
|
||||
onClick={() => router.push((router.query.redirect as string) || "/")}
|
||||
>
|
||||
{t("error.button.back")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
65
frontend/src/pages/share/[shareId]/edit.tsx
Normal file
65
frontend/src/pages/share/[shareId]/edit.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { LoadingOverlay } from "@mantine/core";
|
||||
import { useModals } from "@mantine/modals";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useEffect, useState } from "react";
|
||||
import showErrorModal from "../../../components/share/showErrorModal";
|
||||
import shareService from "../../../services/share.service";
|
||||
import { Share as ShareType } from "../../../types/share.type";
|
||||
import useTranslate from "../../../hooks/useTranslate.hook";
|
||||
import EditableUpload from "../../../components/upload/EditableUpload";
|
||||
import Meta from "../../../components/Meta";
|
||||
|
||||
export function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
return {
|
||||
props: { shareId: context.params!.shareId },
|
||||
};
|
||||
}
|
||||
|
||||
const Share = ({ shareId }: { shareId: string }) => {
|
||||
const t = useTranslate();
|
||||
const modals = useModals();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [share, setShare] = useState<ShareType>();
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.getFromOwner(shareId)
|
||||
.then((share) => {
|
||||
setShare(share);
|
||||
})
|
||||
.catch((e) => {
|
||||
const { error } = e.response.data;
|
||||
if (e.response.status == 404) {
|
||||
if (error == "share_removed") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.removed.title"),
|
||||
e.response.data.message,
|
||||
);
|
||||
} else {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.not-found.title"),
|
||||
t("share.error.not-found.description"),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("share.edit.title", { shareId })} />
|
||||
<EditableUpload shareId={shareId} files={share?.files || []} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
@@ -42,7 +42,14 @@ const Upload = ({
|
||||
|
||||
const uploadFiles = async (share: CreateShare, files: FileUpload[]) => {
|
||||
setisUploading(true);
|
||||
createdShare = await shareService.create(share);
|
||||
|
||||
try {
|
||||
createdShare = await shareService.create(share);
|
||||
} catch (e) {
|
||||
toast.axiosError(e);
|
||||
setisUploading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const fileUploadPromises = files.map(async (file, fileIndex) =>
|
||||
// Limit the number of concurrent uploads to 3
|
||||
@@ -56,7 +63,7 @@ const Upload = ({
|
||||
file.uploadingProgress = progress;
|
||||
}
|
||||
return file;
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -84,7 +91,7 @@ const Upload = ({
|
||||
name: file.name,
|
||||
},
|
||||
chunkIndex,
|
||||
chunks
|
||||
chunks,
|
||||
)
|
||||
.then((response) => {
|
||||
fileId = response.id;
|
||||
@@ -114,7 +121,7 @@ const Upload = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
Promise.all(fileUploadPromises);
|
||||
@@ -129,19 +136,20 @@ const Upload = ({
|
||||
isReverseShare,
|
||||
appUrl: config.get("general.appUrl"),
|
||||
allowUnauthenticatedShares: config.get(
|
||||
"share.allowUnauthenticatedShares"
|
||||
"share.allowUnauthenticatedShares",
|
||||
),
|
||||
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
|
||||
maxExpirationInHours: config.get("share.maxExpiration"),
|
||||
},
|
||||
files,
|
||||
uploadFiles
|
||||
uploadFiles,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there are any files that failed to upload
|
||||
const fileErrorCount = files.filter(
|
||||
(file) => file.uploadingProgress == -1
|
||||
(file) => file.uploadingProgress == -1,
|
||||
).length;
|
||||
|
||||
if (fileErrorCount > 0) {
|
||||
@@ -151,7 +159,7 @@ const Upload = ({
|
||||
{
|
||||
withCloseButton: false,
|
||||
autoClose: false,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
errorToastShown = true;
|
||||
@@ -194,7 +202,9 @@ const Upload = ({
|
||||
showCreateUploadModalCallback={showCreateUploadModalCallback}
|
||||
isUploading={isUploading}
|
||||
/>
|
||||
{files.length > 0 && <FileList files={files} setFiles={setFiles} />}
|
||||
{files.length > 0 && (
|
||||
<FileList<FileUpload> files={files} setFiles={setFiles} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,24 +15,11 @@ const signIn = async (emailOrUsername: string, password: string) => {
|
||||
return response;
|
||||
};
|
||||
|
||||
const signInTotp = async (
|
||||
emailOrUsername: string,
|
||||
password: string,
|
||||
totp: string,
|
||||
loginToken: string,
|
||||
) => {
|
||||
const emailOrUsernameBody = emailOrUsername.includes("@")
|
||||
? { email: emailOrUsername }
|
||||
: { username: emailOrUsername };
|
||||
|
||||
const response = await api.post("auth/signIn/totp", {
|
||||
...emailOrUsernameBody,
|
||||
password,
|
||||
const signInTotp = (totp: string, loginToken: string) => {
|
||||
return api.post("auth/signIn/totp", {
|
||||
totp,
|
||||
loginToken,
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
const signUp = async (email: string, username: string, password: string) => {
|
||||
@@ -96,6 +83,14 @@ const disableTOTP = async (totpCode: string, password: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const getAvailableOAuth = async () => {
|
||||
return api.get("/oauth/available");
|
||||
};
|
||||
|
||||
const getOAuthStatus = () => {
|
||||
return api.get("/oauth/status");
|
||||
};
|
||||
|
||||
export default {
|
||||
signIn,
|
||||
signInTotp,
|
||||
@@ -108,4 +103,6 @@ export default {
|
||||
enableTOTP,
|
||||
verifyTOTP,
|
||||
disableTOTP,
|
||||
getAvailableOAuth,
|
||||
getOAuthStatus,
|
||||
};
|
||||
|
||||
@@ -19,10 +19,18 @@ const completeShare = async (id: string) => {
|
||||
return (await api.post(`shares/${id}/complete`)).data;
|
||||
};
|
||||
|
||||
const revertComplete = async (id: string) => {
|
||||
return (await api.delete(`shares/${id}/complete`)).data;
|
||||
};
|
||||
|
||||
const get = async (id: string): Promise<Share> => {
|
||||
return (await api.get(`shares/${id}`)).data;
|
||||
};
|
||||
|
||||
const getFromOwner = async (id: string): Promise<Share> => {
|
||||
return (await api.get(`shares/${id}/from-owner`)).data;
|
||||
};
|
||||
|
||||
const getMetaData = async (id: string): Promise<ShareMetaData> => {
|
||||
return (await api.get(`shares/${id}/metaData`)).data;
|
||||
};
|
||||
@@ -63,6 +71,10 @@ const downloadFile = async (shareId: string, fileId: string) => {
|
||||
window.location.href = `${window.location.origin}/api/shares/${shareId}/files/${fileId}`;
|
||||
};
|
||||
|
||||
const removeFile = async (shareId: string, fileId: string) => {
|
||||
await api.delete(`shares/${shareId}/files/${fileId}`);
|
||||
};
|
||||
|
||||
const uploadFile = async (
|
||||
shareId: string,
|
||||
readerEvent: ProgressEvent<FileReader>,
|
||||
@@ -121,14 +133,17 @@ const removeReverseShare = async (id: string) => {
|
||||
export default {
|
||||
create,
|
||||
completeShare,
|
||||
revertComplete,
|
||||
getShareToken,
|
||||
get,
|
||||
getFromOwner,
|
||||
remove,
|
||||
getMetaData,
|
||||
doesFileSupportPreview,
|
||||
getMyShares,
|
||||
isShareIdAvailable,
|
||||
downloadFile,
|
||||
removeFile,
|
||||
uploadFile,
|
||||
setReverseShare,
|
||||
createReverseShare,
|
||||
|
||||
@@ -7,3 +7,5 @@ export type FileMetaData = {
|
||||
name: string;
|
||||
size: string;
|
||||
};
|
||||
|
||||
export type FileListItem = FileUpload | (FileMetaData & { deleted?: boolean });
|
||||
|
||||
@@ -4,6 +4,7 @@ type User = {
|
||||
email: string;
|
||||
isAdmin: boolean;
|
||||
totpVerified: boolean;
|
||||
hasPassword: boolean;
|
||||
};
|
||||
|
||||
export type CreateUser = {
|
||||
|
||||
29
frontend/src/utils/oauth.util.tsx
Normal file
29
frontend/src/utils/oauth.util.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
SiDiscord,
|
||||
SiGithub,
|
||||
SiGoogle,
|
||||
SiMicrosoft,
|
||||
SiOpenid,
|
||||
} from "react-icons/si";
|
||||
import React from "react";
|
||||
import api from "../services/api.service";
|
||||
|
||||
const getOAuthUrl = (appUrl: string, provider: string) => {
|
||||
return `${appUrl}/api/oauth/auth/${provider}`;
|
||||
};
|
||||
|
||||
const getOAuthIcon = (provider: string) => {
|
||||
return {
|
||||
google: <SiGoogle />,
|
||||
microsoft: <SiMicrosoft />,
|
||||
github: <SiGithub />,
|
||||
discord: <SiDiscord />,
|
||||
oidc: <SiOpenid />,
|
||||
}[provider];
|
||||
};
|
||||
|
||||
const unlinkOAuth = (provider: string) => {
|
||||
return api.post(`/oauth/unlink/${provider}`);
|
||||
};
|
||||
|
||||
export { getOAuthUrl, getOAuthIcon, unlinkOAuth };
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share",
|
||||
"version": "0.18.2",
|
||||
"version": "0.21.4",
|
||||
"scripts": {
|
||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||
|
||||
Reference in New Issue
Block a user