feat: site wide default language support, translations for backend strings (#70)
* feat(admin): ability to set a default language * feat(backend): translation support for all user facing strings * feat(backend): configure crowdin for backend translations * feat(backend): AI translations for user facing strings * fix(smtp): test email errors are now translated * fix(email): use language locale for time from now
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"plugins": ["@nestjs/swagger"]
|
||||
"plugins": ["@nestjs/swagger"],
|
||||
"deleteOutDir": true,
|
||||
"assets": [{ "include": "i18n/**/*", "watchAssets": true }]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+221
-6
@@ -38,6 +38,7 @@
|
||||
"mime-types": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"nestjs-i18n": "^10.8.4",
|
||||
"nodemailer": "^8.0.4",
|
||||
"otplib": "^13.3.0",
|
||||
"passport": "^0.7.0",
|
||||
@@ -2707,6 +2708,50 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/core": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz",
|
||||
"integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@messageformat/date-skeleton": "^1.0.0",
|
||||
"@messageformat/number-skeleton": "^1.0.0",
|
||||
"@messageformat/parser": "^5.1.0",
|
||||
"@messageformat/runtime": "^3.0.1",
|
||||
"make-plural": "^7.0.0",
|
||||
"safe-identifier": "^0.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/date-skeleton": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz",
|
||||
"integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@messageformat/number-skeleton": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz",
|
||||
"integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@messageformat/parser": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz",
|
||||
"integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moo": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@messageformat/runtime": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.2.tgz",
|
||||
"integrity": "sha512-dkIPDCjXcfhSHgNE1/qV6TeczQZR59Yx0xXeafVKgK3QVWoxc38ljwpksUpnzCGvN151KUbCJTDZVmahtf1YZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"make-plural": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.16.0.tgz",
|
||||
@@ -5130,6 +5175,31 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
@@ -5277,6 +5347,18 @@
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
@@ -5407,7 +5489,6 @@
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@@ -6900,7 +6981,6 @@
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -7223,6 +7303,18 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-to-regexp": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
|
||||
@@ -7420,11 +7512,22 @@
|
||||
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -7441,7 +7544,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
@@ -7462,7 +7564,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -7934,6 +8035,12 @@
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
},
|
||||
"node_modules/make-plural": {
|
||||
"version": "7.5.0",
|
||||
"resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.5.0.tgz",
|
||||
"integrity": "sha512-0booA+aVYyVFoR67JBHdfVk0U08HmrBH2FrtmBqBa+NldlqXv/G2Z9VQuQq6Wgp2jDWdybEWGfBkk1cq5264WA==",
|
||||
"license": "Unicode-DFS-2016"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -8082,6 +8189,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moo": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/moo/-/moo-0.5.3.tgz",
|
||||
"integrity": "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -8155,6 +8268,97 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nestjs-i18n": {
|
||||
"version": "10.8.4",
|
||||
"resolved": "https://registry.npmjs.org/nestjs-i18n/-/nestjs-i18n-10.8.4.tgz",
|
||||
"integrity": "sha512-CJAp+IvaWQaCHi7jI1aej5D4S4uJIIGgqaYT8TrMAW0u3JzLS31FPFMi6OFo7LLcaSgQ7uKPyEOYZe6A3i+N4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@messageformat/core": "^3.4.0",
|
||||
"chokidar": "^3.6.0",
|
||||
"cookie": "^1.1.1",
|
||||
"string-format": "^2.0.0",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"bin": {
|
||||
"nestjs-i18n": "bin/nestjs-i18n.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "*",
|
||||
"@nestjs/core": "*",
|
||||
"class-validator": "*",
|
||||
"rxjs": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/nestjs-i18n/node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/nestjs-i18n/node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/nestjs-i18n/node_modules/picomatch": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/nestjs-i18n/node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
@@ -8946,6 +9150,12 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/safe-identifier": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz",
|
||||
"integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -9310,6 +9520,12 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-format": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/string-format/-/string-format-2.0.0.tgz",
|
||||
"integrity": "sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==",
|
||||
"license": "WTFPL OR MIT"
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -9600,7 +9816,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"mime-types": "^3.0.1",
|
||||
"moment": "^2.30.1",
|
||||
"nanoid": "^5.1.5",
|
||||
"nestjs-i18n": "^10.8.4",
|
||||
"nodemailer": "^8.0.4",
|
||||
"otplib": "^13.3.0",
|
||||
"passport": "^0.7.0",
|
||||
|
||||
@@ -34,6 +34,11 @@ export const configVariables = {
|
||||
defaultValue: "3 months",
|
||||
secret: false,
|
||||
},
|
||||
defaultLanguage: {
|
||||
type: "string",
|
||||
defaultValue: "en-US",
|
||||
secret: false,
|
||||
},
|
||||
},
|
||||
appearance: {
|
||||
themePrimaryColor: {
|
||||
|
||||
@@ -3,6 +3,10 @@ import { Module } from "@nestjs/common";
|
||||
import { ScheduleModule } from "@nestjs/schedule";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
|
||||
import { existsSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { I18nModule } from "nestjs-i18n";
|
||||
|
||||
import { APP_GUARD } from "@nestjs/core";
|
||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { AppCacheModule } from "./cache/cache.module";
|
||||
@@ -19,6 +23,12 @@ import { ShareModule } from "./share/share.module";
|
||||
import { UserModule } from "./user/user.module";
|
||||
import { SystemModule } from "./system/system.module";
|
||||
|
||||
import { SystemLanguageResolver } from "./i18n/systemLanguage.resolver";
|
||||
|
||||
const i18nPath = existsSync(join(__dirname, "../i18n"))
|
||||
? join(__dirname, "../i18n")
|
||||
: join(__dirname, "i18n");
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule,
|
||||
@@ -41,6 +51,14 @@ import { SystemModule } from "./system/system.module";
|
||||
ReverseShareModule,
|
||||
OAuthModule,
|
||||
AppCacheModule,
|
||||
I18nModule.forRoot({
|
||||
fallbackLanguage: "en-US",
|
||||
loaderOptions: {
|
||||
path: i18nPath,
|
||||
watch: true,
|
||||
},
|
||||
resolvers: [SystemLanguageResolver],
|
||||
}),
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
@@ -48,6 +66,7 @@ import { SystemModule } from "./system/system.module";
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
SystemLanguageResolver,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { Throttle } from "@nestjs/throttler";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request, Response } from "express";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { AuthTotpService } from "./authTotp.service";
|
||||
@@ -34,6 +35,7 @@ export class AuthController {
|
||||
private authService: AuthService,
|
||||
private authTotpService: AuthTotpService,
|
||||
private config: ConfigService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
@Post("signUp")
|
||||
@@ -49,7 +51,7 @@ export class AuthController {
|
||||
@Res({ passthrough: true }) response: Response,
|
||||
) {
|
||||
if (!this.config.get("share.allowRegistration"))
|
||||
throw new ForbiddenException("Registration is not allowed");
|
||||
throw new ForbiddenException(this.i18n.t("auth.registrationNotAllowed"));
|
||||
|
||||
const result = await this.authService.signUp(dto, ip);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
||||
import * as argon from "argon2";
|
||||
import { Request, Response } from "express";
|
||||
import * as moment from "moment";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { EmailService } from "src/email/email.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
@@ -33,6 +34,7 @@ export class AuthService {
|
||||
private ldapService: LdapService,
|
||||
private userService: UserSevice,
|
||||
@Inject(forwardRef(() => OAuthService)) private oAuthService: OAuthService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
@@ -62,7 +64,9 @@ export class AuthService {
|
||||
if (e.code == "P2002") {
|
||||
const duplicatedField: string = e.meta.target[0];
|
||||
throw new BadRequestException(
|
||||
`A user with this ${duplicatedField} already exists`,
|
||||
this.i18n.t("auth.userAlreadyExists", {
|
||||
args: { field: duplicatedField },
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export class AuthService {
|
||||
|
||||
async signIn(dto: AuthSignInDTO, ip: string) {
|
||||
if (!dto.email && !dto.username) {
|
||||
throw new BadRequestException("Email or username is required");
|
||||
throw new BadRequestException(this.i18n.t("auth.emailOrUsernameRequired"));
|
||||
}
|
||||
|
||||
if (!this.config.get("oauth.disablePassword")) {
|
||||
@@ -114,7 +118,7 @@ export class AuthService {
|
||||
this.logger.log(
|
||||
`Failed login attempt for user ${dto.email || dto.username} from IP ${ip}`,
|
||||
);
|
||||
throw new UnauthorizedException("Wrong email or password");
|
||||
throw new UnauthorizedException(this.i18n.t("auth.wrongCredentials"));
|
||||
}
|
||||
|
||||
async generateToken(user: User, oauth?: { idToken?: string }) {
|
||||
@@ -137,7 +141,7 @@ export class AuthService {
|
||||
|
||||
async requestResetPassword(email: string) {
|
||||
if (this.config.get("oauth.disablePassword"))
|
||||
throw new ForbiddenException("Password sign in is disabled");
|
||||
throw new ForbiddenException(this.i18n.t("auth.passwordSignInDisabled"));
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { email },
|
||||
@@ -150,9 +154,7 @@ export class AuthService {
|
||||
this.logger.log(
|
||||
`Failed password reset request for user ${email} because it is an LDAP user`,
|
||||
);
|
||||
throw new BadRequestException(
|
||||
"This account can't reset its password here. Please contact your administrator.",
|
||||
);
|
||||
throw new BadRequestException(this.i18n.t("auth.ldapResetPasswordNotAllowed"));
|
||||
}
|
||||
|
||||
// Delete old reset password token
|
||||
@@ -174,13 +176,13 @@ export class AuthService {
|
||||
|
||||
async resetPassword(token: string, newPassword: string) {
|
||||
if (this.config.get("oauth.disablePassword"))
|
||||
throw new ForbiddenException("Password sign in is disabled");
|
||||
throw new ForbiddenException(this.i18n.t("auth.passwordSignInDisabled"));
|
||||
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { resetPasswordToken: { token } },
|
||||
});
|
||||
|
||||
if (!user) throw new BadRequestException("Token invalid or expired");
|
||||
if (!user) throw new BadRequestException(this.i18n.t("auth.tokenInvalidOrExpired"));
|
||||
|
||||
const newPasswordHash = await argon.hash(newPassword);
|
||||
|
||||
@@ -198,7 +200,7 @@ export class AuthService {
|
||||
const isPasswordValid =
|
||||
!user.password || (await argon.verify(user.password, oldPassword));
|
||||
|
||||
if (!isPasswordValid) throw new ForbiddenException("Invalid password");
|
||||
if (!isPasswordValid) throw new ForbiddenException(this.i18n.t("auth.invalidPassword"));
|
||||
|
||||
const hash = await argon.hash(newPassword);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
createGuardrails,
|
||||
} from "otplib";
|
||||
import * as qrcode from "qrcode-svg";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { AuthService } from "./auth.service";
|
||||
@@ -28,6 +29,7 @@ export class AuthTotpService {
|
||||
private prisma: PrismaService,
|
||||
private configService: ConfigService,
|
||||
private authService: AuthService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
async signInTotp(dto: AuthSignInTotpDTO) {
|
||||
@@ -41,16 +43,19 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (!token || token.used)
|
||||
throw new UnauthorizedException("Invalid login token");
|
||||
throw new UnauthorizedException(this.i18n.t("auth.invalidLoginToken"));
|
||||
|
||||
if (token.expiresAt < new Date())
|
||||
throw new UnauthorizedException("Login token expired", "token_expired");
|
||||
throw new UnauthorizedException(
|
||||
this.i18n.t("auth.loginTokenExpired"),
|
||||
"token_expired",
|
||||
);
|
||||
|
||||
// Check the TOTP code
|
||||
const { totpSecret } = token.user;
|
||||
|
||||
if (!totpSecret) {
|
||||
throw new BadRequestException("TOTP is not enabled");
|
||||
throw new BadRequestException(this.i18n.t("auth.totpNotEnabled"));
|
||||
}
|
||||
|
||||
const verified = await verify({
|
||||
@@ -59,7 +64,7 @@ export class AuthTotpService {
|
||||
guardrails: legacyGuardrails,
|
||||
});
|
||||
if (!verified.valid) {
|
||||
throw new BadRequestException("Invalid code");
|
||||
throw new BadRequestException(this.i18n.t("auth.invalidCode"));
|
||||
}
|
||||
|
||||
// Set the login token to used
|
||||
@@ -80,7 +85,7 @@ export class AuthTotpService {
|
||||
|
||||
async enableTotp(user: User, password: string) {
|
||||
if (!this.authService.verifyPassword(user, password))
|
||||
throw new ForbiddenException("Invalid password");
|
||||
throw new ForbiddenException(this.i18n.t("auth.invalidPassword"));
|
||||
|
||||
// Check if we have a secret already
|
||||
const { totpVerified } = await this.prisma.user.findUnique({
|
||||
@@ -89,7 +94,7 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (totpVerified) {
|
||||
throw new BadRequestException("TOTP is already enabled");
|
||||
throw new BadRequestException(this.i18n.t("auth.totpAlreadyEnabled"));
|
||||
}
|
||||
|
||||
const issuer = this.configService.get("general.appName");
|
||||
@@ -126,7 +131,7 @@ export class AuthTotpService {
|
||||
|
||||
async verifyTotp(user: User, password: string, code: string) {
|
||||
if (!this.authService.verifyPassword(user, password))
|
||||
throw new ForbiddenException("Invalid password");
|
||||
throw new ForbiddenException(this.i18n.t("auth.invalidPassword"));
|
||||
|
||||
const { totpSecret } = await this.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
@@ -134,7 +139,7 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (!totpSecret) {
|
||||
throw new BadRequestException("TOTP is not in progress");
|
||||
throw new BadRequestException(this.i18n.t("auth.totpNotInProgress"));
|
||||
}
|
||||
|
||||
const expected = await generate({
|
||||
@@ -143,7 +148,7 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (code !== expected) {
|
||||
throw new BadRequestException("Invalid code");
|
||||
throw new BadRequestException(this.i18n.t("auth.invalidCode"));
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
@@ -158,7 +163,7 @@ export class AuthTotpService {
|
||||
|
||||
async disableTotp(user: User, password: string, code: string) {
|
||||
if (!this.authService.verifyPassword(user, password))
|
||||
throw new ForbiddenException("Invalid password");
|
||||
throw new ForbiddenException(this.i18n.t("auth.invalidPassword"));
|
||||
|
||||
const { totpSecret } = await this.prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
@@ -166,7 +171,7 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (!totpSecret) {
|
||||
throw new BadRequestException("TOTP is not enabled");
|
||||
throw new BadRequestException(this.i18n.t("auth.totpNotEnabled"));
|
||||
}
|
||||
|
||||
const expected = await generate({
|
||||
@@ -175,7 +180,7 @@ export class AuthTotpService {
|
||||
});
|
||||
|
||||
if (code !== expected) {
|
||||
throw new BadRequestException("Invalid code");
|
||||
throw new BadRequestException(this.i18n.t("auth.invalidCode"));
|
||||
}
|
||||
|
||||
await this.prisma.user.update({
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { SkipThrottle } from "@nestjs/throttler";
|
||||
import { createKeyv } from "@keyv/redis";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { AdministratorGuard } from "src/auth/guard/isAdmin.guard";
|
||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||
import { EmailService } from "src/email/email.service";
|
||||
@@ -32,6 +33,7 @@ export class ConfigController {
|
||||
private configService: ConfigService,
|
||||
private logoService: LogoService,
|
||||
private emailService: EmailService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@@ -69,7 +71,7 @@ export class ConfigController {
|
||||
const enabled = this.configService.get("cache.redis-enabled");
|
||||
|
||||
if (!redisUrl) {
|
||||
throw new InternalServerErrorException("Redis URL is not set");
|
||||
throw new InternalServerErrorException(this.i18n.t("config.redisUrlNotSet"));
|
||||
}
|
||||
|
||||
const withTimeout = async <T>(
|
||||
@@ -82,7 +84,12 @@ export class ConfigController {
|
||||
promise,
|
||||
new Promise<T>((_, reject) => {
|
||||
timeout = setTimeout(
|
||||
() => reject(new GatewayTimeoutException("Redis timed out")),
|
||||
() =>
|
||||
reject(
|
||||
new GatewayTimeoutException(
|
||||
this.i18n.t("config.redisTimedOut"),
|
||||
),
|
||||
),
|
||||
timeoutMs,
|
||||
);
|
||||
}),
|
||||
@@ -97,7 +104,8 @@ export class ConfigController {
|
||||
url: redisUrl,
|
||||
socket: {
|
||||
connectTimeout: 3000,
|
||||
reconnectStrategy: () => new Error("Redis connection failed"),
|
||||
reconnectStrategy: () =>
|
||||
new Error(this.i18n.t("config.redisConnectionFailed")),
|
||||
},
|
||||
} as any,
|
||||
{ namespace: "pingvin" },
|
||||
@@ -108,7 +116,7 @@ export class ConfigController {
|
||||
await withTimeout(keyv.set(testKey, "ok", 5000), 5000);
|
||||
const value = await withTimeout(keyv.get(testKey), 5000);
|
||||
if (value !== "ok") {
|
||||
throw new Error("Unexpected response from Redis");
|
||||
throw new Error(this.i18n.t("config.redisUnexpectedResponse"));
|
||||
}
|
||||
|
||||
return { ok: true, enabled };
|
||||
@@ -117,7 +125,7 @@ export class ConfigController {
|
||||
const message =
|
||||
typeof e?.message === "string"
|
||||
? `${e?.name ? `${e.name}: ` : ""}${e.message}`
|
||||
: "Redis error";
|
||||
: this.i18n.t("config.redisError");
|
||||
throw new InternalServerErrorException(message);
|
||||
} finally {
|
||||
const store: any = (keyv as any).store;
|
||||
|
||||
@@ -12,6 +12,7 @@ import * as fs from "fs";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { stringToTimespan } from "src/utils/date.util";
|
||||
import { parse as yamlParse } from "yaml";
|
||||
import { I18nContext } from "nestjs-i18n";
|
||||
import { YamlConfig } from "../../prisma/seed/config.seed";
|
||||
import { CONFIG_FILE } from "src/constants";
|
||||
|
||||
@@ -139,7 +140,10 @@ export class ConfigService extends EventEmitter {
|
||||
async updateMany(data: { key: string; value: string | number | boolean }[]) {
|
||||
if (!this.isEditAllowed())
|
||||
throw new BadRequestException(
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
this.t(
|
||||
"config.editNotAllowed",
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
),
|
||||
);
|
||||
|
||||
const response: Config[] = [];
|
||||
@@ -154,7 +158,10 @@ export class ConfigService extends EventEmitter {
|
||||
async update(key: string, value: string | number | boolean) {
|
||||
if (!this.isEditAllowed())
|
||||
throw new BadRequestException(
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
this.t(
|
||||
"config.editNotAllowed",
|
||||
"You are only allowed to update config variables via the config.yaml file",
|
||||
),
|
||||
);
|
||||
|
||||
const configVariable = await this.prisma.config.findUnique({
|
||||
@@ -167,7 +174,9 @@ export class ConfigService extends EventEmitter {
|
||||
});
|
||||
|
||||
if (!configVariable || configVariable.locked)
|
||||
throw new NotFoundException("Config variable not found");
|
||||
throw new NotFoundException(
|
||||
this.t("config.variableNotFound", "Config variable not found"),
|
||||
);
|
||||
|
||||
if (value === "") {
|
||||
value = null;
|
||||
@@ -178,7 +187,9 @@ export class ConfigService extends EventEmitter {
|
||||
configVariable.type != "timespan"
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Config variable must be of type ${configVariable.type}`,
|
||||
this.t("config.invalidType", "Config variable must be of type {type}", {
|
||||
type: configVariable.type,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,12 +217,18 @@ export class ConfigService extends EventEmitter {
|
||||
{
|
||||
key: "share.shareIdLength",
|
||||
condition: (value: number) => value >= 2 && value <= 50,
|
||||
message: "Share ID length must be between 2 and 50",
|
||||
message: this.t(
|
||||
"config.shareIdLengthValidation",
|
||||
"Share ID length must be between 2 and 50",
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "share.zipCompressionLevel",
|
||||
condition: (value: number) => value >= 0 && value <= 9,
|
||||
message: "Zip compression level must be between 0 and 9",
|
||||
message: this.t(
|
||||
"config.zipCompressionLevelValidation",
|
||||
"Zip compression level must be between 0 and 9",
|
||||
),
|
||||
},
|
||||
// TODO add validation for timespan type
|
||||
];
|
||||
@@ -225,4 +242,19 @@ export class ConfigService extends EventEmitter {
|
||||
isEditAllowed(): boolean {
|
||||
return this.yamlConfig === undefined || this.yamlConfig === null;
|
||||
}
|
||||
|
||||
private t(
|
||||
key: string,
|
||||
fallback: string,
|
||||
args?: Record<string, string | number | boolean>,
|
||||
) {
|
||||
const translated = I18nContext.current()?.t(key, { args });
|
||||
if (translated && translated !== key) return translated;
|
||||
|
||||
return Object.entries(args ?? {}).reduce(
|
||||
(message, [argKey, value]) =>
|
||||
message.replaceAll(`{${argKey}}`, String(value)),
|
||||
fallback,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,16 +6,20 @@ import {
|
||||
import { User } from "@prisma/client";
|
||||
import * as moment from "moment";
|
||||
import * as nodemailer from "nodemailer";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
constructor(private config: ConfigService) {}
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
|
||||
getTransporter() {
|
||||
if (!this.config.get("smtp.enabled"))
|
||||
throw new InternalServerErrorException("SMTP is disabled");
|
||||
throw new InternalServerErrorException(this.i18n.t("email.smtpDisabled"));
|
||||
|
||||
const username = this.config.get("smtp.username");
|
||||
const password = this.config.get("smtp.password");
|
||||
@@ -46,7 +50,7 @@ export class EmailService {
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
throw new InternalServerErrorException("Failed to send email");
|
||||
throw new InternalServerErrorException(this.i18n.t("email.sendFailed"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,9 +62,13 @@ export class EmailService {
|
||||
expiration?: Date,
|
||||
) {
|
||||
if (!this.config.get("email.enableShareEmailRecipients"))
|
||||
throw new InternalServerErrorException("Email service disabled");
|
||||
throw new InternalServerErrorException(
|
||||
this.i18n.t("email.emailServiceDisabled"),
|
||||
);
|
||||
|
||||
const shareUrl = `${this.config.get("general.appUrl")}/s/${shareId}`;
|
||||
const lang = "";
|
||||
const locale = this.i18n.translate("email.locale", { lang });
|
||||
|
||||
await this.sendMail(
|
||||
recipientEmail,
|
||||
@@ -68,15 +76,22 @@ export class EmailService {
|
||||
this.config
|
||||
.get("email.shareRecipientsMessage")
|
||||
.replaceAll("\\n", "\n")
|
||||
.replaceAll("{creator}", creator?.username ?? "Someone")
|
||||
.replaceAll(
|
||||
"{creator}",
|
||||
creator?.username ??
|
||||
this.i18n.t("email.shareRecipientsCreatorFallback"),
|
||||
)
|
||||
.replaceAll("{creatorEmail}", creator?.email ?? "")
|
||||
.replaceAll("{shareUrl}", shareUrl)
|
||||
.replaceAll("{desc}", description ?? "No description")
|
||||
.replaceAll(
|
||||
"{desc}",
|
||||
description ?? this.i18n.t("email.shareRecipientsDescFallback"),
|
||||
)
|
||||
.replaceAll(
|
||||
"{expires}",
|
||||
moment(expiration).unix() != 0
|
||||
? moment(expiration).fromNow()
|
||||
: "in: never",
|
||||
? moment(expiration).locale(locale).fromNow()
|
||||
: this.i18n.t("email.shareRecipientsExpiresNeverFallback"),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -124,14 +139,16 @@ export class EmailService {
|
||||
}
|
||||
|
||||
async sendTestMail(recipientEmail: string) {
|
||||
const subject = this.i18n.t("email.testSubject");
|
||||
const text = this.i18n.t("email.testText");
|
||||
await this.getTransporter()
|
||||
.sendMail({
|
||||
from: `"${this.config.get("general.appName")}" <${this.config.get(
|
||||
"smtp.email",
|
||||
)}>`,
|
||||
to: recipientEmail,
|
||||
subject: "Test email",
|
||||
text: "This is a test email",
|
||||
subject,
|
||||
text,
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(e);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { Request } from "express";
|
||||
import * as moment from "moment";
|
||||
import { User } from "@prisma/client";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
@@ -19,8 +20,9 @@ export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
private _shareService: ShareService,
|
||||
private _prisma: PrismaService,
|
||||
private _config: ConfigService,
|
||||
private readonly _i18n: I18nService,
|
||||
) {
|
||||
super(_shareService, _prisma, _config);
|
||||
super(_shareService, _prisma, _config, _i18n);
|
||||
}
|
||||
|
||||
isBase64(toCheck: string) {
|
||||
@@ -39,7 +41,7 @@ export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
: request.params.id;
|
||||
|
||||
if (!this.isBase64(shareId)) {
|
||||
throw new BadRequestException("Invalid ID format");
|
||||
throw new BadRequestException(this._i18n.t("file.invalidIdFormat"));
|
||||
}
|
||||
|
||||
const shareToken = request.cookies[`share_${shareId}_token`];
|
||||
@@ -65,15 +67,15 @@ export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
(moment().isAfter(share.expiration) &&
|
||||
!moment(share.expiration).isSame(0))
|
||||
) {
|
||||
throw new NotFoundException("File not found");
|
||||
throw new NotFoundException(this._i18n.t("file.notFound"));
|
||||
}
|
||||
|
||||
if (share.security?.password)
|
||||
throw new ForbiddenException("This share is password protected");
|
||||
throw new ForbiddenException(this._i18n.t("file.passwordProtected"));
|
||||
|
||||
if (share.security?.maxViews && share.security.maxViews <= share.views) {
|
||||
throw new ForbiddenException(
|
||||
"Maximum views exceeded",
|
||||
this._i18n.t("share.maxViewsExceeded"),
|
||||
"share_max_views_exceeded",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import * as crypto from "crypto";
|
||||
import { createReadStream } from "fs";
|
||||
import * as fs from "fs/promises";
|
||||
import * as mime from "mime-types";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { validate as isValidUUID } from "uuid";
|
||||
@@ -21,6 +22,7 @@ export class LocalFileService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private config: ConfigService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@@ -32,7 +34,7 @@ export class LocalFileService {
|
||||
if (!file.id) {
|
||||
file.id = crypto.randomUUID();
|
||||
} else if (!isValidUUID(file.id)) {
|
||||
throw new BadRequestException("Invalid file ID format");
|
||||
throw new BadRequestException(this.i18n.t("file.invalidIdFormat"));
|
||||
}
|
||||
|
||||
const share = await this.prisma.share.findUnique({
|
||||
@@ -41,7 +43,7 @@ export class LocalFileService {
|
||||
});
|
||||
|
||||
if (share.uploadLocked)
|
||||
throw new BadRequestException("Share is already completed");
|
||||
throw new BadRequestException(this.i18n.t("file.alreadyCompleted"));
|
||||
|
||||
let diskFileSize: number;
|
||||
try {
|
||||
@@ -58,7 +60,7 @@ export class LocalFileService {
|
||||
|
||||
if (expectedChunkIndex != chunk.index)
|
||||
throw new BadRequestException({
|
||||
message: "Unexpected chunk received",
|
||||
message: this.i18n.t("file.unexpectedChunk"),
|
||||
error: "unexpected_chunk_index",
|
||||
expectedChunkIndex,
|
||||
});
|
||||
@@ -69,7 +71,7 @@ export class LocalFileService {
|
||||
const space = await fs.statfs(SHARE_DIRECTORY);
|
||||
const availableSpace = space.bavail * space.bsize;
|
||||
if (availableSpace < buffer.byteLength) {
|
||||
throw new InternalServerErrorException("Not enough space on the server");
|
||||
throw new InternalServerErrorException(this.i18n.t("file.notEnoughSpace"));
|
||||
}
|
||||
|
||||
// Check if share size limit is exceeded
|
||||
@@ -86,7 +88,7 @@ export class LocalFileService {
|
||||
shareSizeSum > parseInt(share.reverseShare.maxShareSize))
|
||||
) {
|
||||
throw new HttpException(
|
||||
"Max share size exceeded",
|
||||
this.i18n.t("file.maxSizeExceeded"),
|
||||
HttpStatus.PAYLOAD_TOO_LARGE,
|
||||
);
|
||||
}
|
||||
@@ -123,7 +125,7 @@ export class LocalFileService {
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
if (!fileMetaData) throw new NotFoundException(this.i18n.t("file.notFound"));
|
||||
|
||||
const file = createReadStream(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||
|
||||
@@ -142,7 +144,7 @@ export class LocalFileService {
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
if (!fileMetaData) throw new NotFoundException(this.i18n.t("file.notFound"));
|
||||
|
||||
await fs.unlink(`${SHARE_DIRECTORY}/${shareId}/${fileId}`);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { I18nService } from "nestjs-i18n";
|
||||
import * as crypto from "crypto";
|
||||
import * as mime from "mime-types";
|
||||
import { File } from "./file.service";
|
||||
@@ -42,6 +43,7 @@ export class S3FileService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private config: ConfigService,
|
||||
private readonly i18n: I18nService,
|
||||
) {}
|
||||
|
||||
async create(
|
||||
@@ -53,7 +55,7 @@ export class S3FileService {
|
||||
if (!file.id) {
|
||||
file.id = crypto.randomUUID();
|
||||
} else if (!isValidUUID(file.id)) {
|
||||
throw new BadRequestException("Invalid file ID format");
|
||||
throw new BadRequestException(this.i18n.t("file.invalidIdFormat"));
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(data, "base64");
|
||||
@@ -73,7 +75,7 @@ export class S3FileService {
|
||||
|
||||
const uploadId = multipartInitResponse.UploadId;
|
||||
if (!uploadId) {
|
||||
throw new Error("Failed to initialize multipart upload.");
|
||||
throw new Error(this.i18n.t("file.s3UploadInitError"));
|
||||
}
|
||||
|
||||
// Store the uploadId and parts list in memory
|
||||
@@ -87,7 +89,7 @@ export class S3FileService {
|
||||
const multipartUpload = this.multipartUploads[file.id];
|
||||
if (!multipartUpload) {
|
||||
throw new InternalServerErrorException(
|
||||
"Multipart upload session not found.",
|
||||
this.i18n.t("file.s3SessionNotFound"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +148,7 @@ export class S3FileService {
|
||||
delete this.multipartUploads[file.id];
|
||||
}
|
||||
this.logger.error(error);
|
||||
throw new Error("Multipart upload failed. The upload has been aborted.");
|
||||
throw new Error(this.i18n.t("file.s3UploadFailed"));
|
||||
}
|
||||
|
||||
const isLastChunk = chunk.index == chunk.total - 1;
|
||||
@@ -200,7 +202,7 @@ export class S3FileService {
|
||||
where: { id: fileId },
|
||||
});
|
||||
|
||||
if (!fileMetaData) throw new NotFoundException("File not found");
|
||||
if (!fileMetaData) throw new NotFoundException(this.i18n.t("file.notFound"));
|
||||
|
||||
const key = `${this.getS3Path()}${shareId}/${fileMetaData.name}`;
|
||||
const s3Instance = this.getS3Instance();
|
||||
@@ -213,7 +215,7 @@ export class S3FileService {
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error("Could not delete file from S3");
|
||||
throw new Error(this.i18n.t("file.s3DeleteError"));
|
||||
}
|
||||
|
||||
await this.prisma.file.delete({ where: { id: fileId } });
|
||||
@@ -296,7 +298,7 @@ export class S3FileService {
|
||||
// Return ContentLength which is the file size in bytes
|
||||
return headObjectResponse.ContentLength ?? 0;
|
||||
} catch (error) {
|
||||
throw new Error("Could not retrieve file size");
|
||||
throw new Error(this.i18n.t("file.s3SizeError"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +345,7 @@ export class S3FileService {
|
||||
|
||||
archive.on("error", (err) => {
|
||||
this.logger.error("Archive error", err);
|
||||
reject(new InternalServerErrorException("Error creating ZIP file"));
|
||||
reject(new InternalServerErrorException(this.i18n.t("file.zipError")));
|
||||
});
|
||||
|
||||
const fileKeys = listResponse.Contents.filter(
|
||||
@@ -403,7 +405,7 @@ export class S3FileService {
|
||||
} catch (error) {
|
||||
this.logger.error("Error creating ZIP file", error);
|
||||
|
||||
reject(new InternalServerErrorException("Error creating ZIP file"));
|
||||
reject(new InternalServerErrorException(this.i18n.t("file.zipError")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "التسجيل غير مسموح به",
|
||||
"emailOrUsernameRequired": "البريد الإلكتروني أو اسم المستخدم مطلوب",
|
||||
"wrongCredentials": "البريد الإلكتروني أو كلمة المرور غير صحيحة",
|
||||
"passwordSignInDisabled": "تسجيل الدخول بكلمة المرور معطل",
|
||||
"tokenInvalidOrExpired": "الرمز غير صالح أو منتهي الصلاحية",
|
||||
"invalidPassword": "كلمة مرور غير صالحة",
|
||||
"invalidLoginToken": "رمز تسجيل دخول غير صالح",
|
||||
"loginTokenExpired": "انتهت صلاحية رمز تسجيل الدخول",
|
||||
"totpNotEnabled": "TOTP غير مفعل",
|
||||
"invalidCode": "رمز غير صالح",
|
||||
"totpAlreadyEnabled": "TOTP مفعل بالفعل",
|
||||
"totpNotInProgress": "TOTP ليس قيد التنفيذ",
|
||||
"userNotFound": "المستخدم غير موجود",
|
||||
"cannotDeleteLastAdmin": "لا يمكن حذف آخر مستخدم مسؤول",
|
||||
"ldapResetPasswordNotAllowed": "لا يمكن لهذا الحساب إعادة تعيين كلمة المرور هنا. يرجى الاتصال بالمسؤول.",
|
||||
"userAlreadyExists": "يوجد مستخدم بهذا {field} بالفعل"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "متغير التكوين غير موجود",
|
||||
"editNotAllowed": "يسمح لك فقط بتحديث متغيرات التكوين عبر ملف config.yaml",
|
||||
"invalidType": "يجب أن يكون متغير التكوين من نوع {type}",
|
||||
"shareIdLengthValidation": "يجب أن يكون طول معرف المشاركة بين 2 و 50",
|
||||
"zipCompressionLevelValidation": "يجب أن يكون مستوى ضغط Zip بين 0 و 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "شخص ما",
|
||||
"shareRecipientsDescFallback": "لا يوجد وصف",
|
||||
"shareRecipientsExpiresNeverFallback": "في: أبداً",
|
||||
"smtpDisabled": "SMTP معطل",
|
||||
"emailServiceDisabled": "خدمة البريد الإلكتروني معطلة",
|
||||
"testSubject": "بريد إلكتروني تجريبي",
|
||||
"testText": "هذا بريد إلكتروني تجريبي",
|
||||
"sendFailed": "فشل إرسال البريد الإلكتروني",
|
||||
"locale": "ar-EG"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "تنسيق المعرف غير صالح",
|
||||
"notFound": "الملف غير موجود",
|
||||
"passwordProtected": "هذه المشاركة محمية بكلمة مرور",
|
||||
"alreadyCompleted": "المشاركة مكتملة بالفعل",
|
||||
"notEnoughSpace": "لا توجد مساحة كافية على الخادم",
|
||||
"unexpectedChunk": "تم استلام قطعة غير متوقعة",
|
||||
"maxSizeExceeded": "تم تجاوز الحد الأقصى لحجم المشاركة",
|
||||
"zipError": "خطأ في إنشاء ملف ZIP",
|
||||
"s3DeleteError": "تعذر حذف الملف من S3",
|
||||
"s3UploadInitError": "فشل بدء التحميل المتعدد الأجزاء.",
|
||||
"s3SessionNotFound": "جلسة التحميل المتعدد الأجزاء غير موجودة.",
|
||||
"s3UploadFailed": "فشل التحميل المتعدد الأجزاء. تم إلغاء التحميل.",
|
||||
"s3SizeError": "تعذر استرداد حجم الملف"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "عنوان URL لخادم LDAP غير محدد",
|
||||
"bindFailed": "فشل الارتباط بالمستخدم الافتراضي"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "هذا الحساب مرتبط بالفعل بمستخدم آخر",
|
||||
"not_linked": "هذا الحساب غير مرتبط بأي مستخدم",
|
||||
"no_user": "لم يتم العثور على مستخدم لهذا الحساب",
|
||||
"no_email": "لم يتم العثور على عنوان بريد إلكتروني لهذا الحساب",
|
||||
"unverified_account": "حسابك غير موثق",
|
||||
"user_not_allowed": "غير مسموح لك بتسجيل الدخول",
|
||||
"invalid_token": "رمز غير صالح",
|
||||
"cannot_get_user_info": "تعذر الحصول على معلومات المستخدم",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "رمز المشاركة العكسية غير موجود",
|
||||
"maxShareSizeExceeded": "لا يمكن أن يكون الحد الأقصى لحجم المشاركة أكبر من {maxSize} بايت."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "لا توجد مساحة قرص كافية متاحة",
|
||||
"idInUse": "معرف المشاركة قيد الاستخدام بالفعل",
|
||||
"alreadyCompleted": "المشاركة مكتملة بالفعل",
|
||||
"completionRequiresFile": "تحتاج إلى ملف واحد على الأقل في مشاركتك لإكمالها.",
|
||||
"notFound": "المشاركة غير موجودة",
|
||||
"anonymousNoDelete": "لا يمكن حذف المشاركات المجهولة",
|
||||
"anonymousNoExpire": "لا يمكن أن تنتهي صلاحية المشاركات المجهولة",
|
||||
"anonymousNoUpdate": "لا يمكن تحديث المشاركات المجهولة",
|
||||
"invalidExpiration": "تاريخ انتهاء صلاحية غير صالح",
|
||||
"maxExpirationExceeded": "تاريخ انتهاء الصلاحية يتجاوز الحد الأقصى لتاريخ انتهاء الصلاحية",
|
||||
"wrongPassword": "كلمة مرور خاطئة",
|
||||
"tokenRequired": "رمز المشاركة مطلوب",
|
||||
"privateShare": "يمكن لمنشئ المشاركة العكسية فقط الوصول إلى هذه المشاركة",
|
||||
"maxViewsExceeded": "تم تجاوز الحد الأقصى للمشاهدات"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "يمكن أن يحتوي اسم المستخدم فقط على أحرف وأرقام ونقاط وشرطات سفلية",
|
||||
"idPattern": "يمكن أن يحتوي المعرف فقط على أحرف وأرقام وشرطات سفلية وشرطات"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Registrace není povolena",
|
||||
"emailOrUsernameRequired": "Email nebo uživatelské jméno je povinné",
|
||||
"wrongCredentials": "Nesprávný email nebo heslo",
|
||||
"passwordSignInDisabled": "Přihlášení heslem je zakázáno",
|
||||
"tokenInvalidOrExpired": "Token je neplatný nebo vypršel",
|
||||
"invalidPassword": "Neplatné heslo",
|
||||
"invalidLoginToken": "Neplatný přihlašovací token",
|
||||
"loginTokenExpired": "Platnost přihlašovacího tokenu vypršela",
|
||||
"totpNotEnabled": "TOTP není povoleno",
|
||||
"invalidCode": "Neplatný kód",
|
||||
"totpAlreadyEnabled": "TOTP je již povoleno",
|
||||
"totpNotInProgress": "TOTP neprobíhá",
|
||||
"userNotFound": "Uživatel nenalezen",
|
||||
"cannotDeleteLastAdmin": "Nelze smazat posledního administrátora",
|
||||
"ldapResetPasswordNotAllowed": "U tohoto účtu nelze zde resetovat heslo. Kontaktujte svého administrátora.",
|
||||
"userAlreadyExists": "Uživatel s tímto polem {field} již existuje"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Konfirační proměnná nenalezena",
|
||||
"editNotAllowed": "Konfirační proměnné můžete aktualizovat pouze pomocí souboru config.yaml",
|
||||
"invalidType": "Konfirační proměnná musí být typu {type}",
|
||||
"shareIdLengthValidation": "Délka ID sdílení musí být mezi 2 a 50",
|
||||
"zipCompressionLevelValidation": "Úroveň komprese Zip musí být mezi 0 a 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Někdo",
|
||||
"shareRecipientsDescFallback": "Bez popisu",
|
||||
"shareRecipientsExpiresNeverFallback": "v: nikdy",
|
||||
"smtpDisabled": "SMTP je zakázáno",
|
||||
"emailServiceDisabled": "Emailová služba je zakázána",
|
||||
"testSubject": "Testovací email",
|
||||
"testText": "Toto je testovací email",
|
||||
"sendFailed": "Odeslání emailu se nezdařilo",
|
||||
"locale": "cs-CZ"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Neplatný formát ID",
|
||||
"notFound": "Soubor nenalezen",
|
||||
"passwordProtected": "Toto sdílení je chráněno heslem",
|
||||
"alreadyCompleted": "Sdílení je již dokončeno",
|
||||
"notEnoughSpace": "Nedostatek místa na serveru",
|
||||
"unexpectedChunk": "Přijat neočekávaný blok",
|
||||
"maxSizeExceeded": "Maximální velikost sdílení byla překročena",
|
||||
"zipError": "Chyba při vytváření ZIP souboru",
|
||||
"s3DeleteError": "Nepodařilo se smazat soubor z S3",
|
||||
"s3UploadInitError": "Nepodařilo se inicializovat nahrávání.",
|
||||
"s3SessionNotFound": "Relace nahrávání nebyla nalezena.",
|
||||
"s3UploadFailed": "Nahrávání se nezdařilo. Nahrávání bylo přerušeno.",
|
||||
"s3SizeError": "Nepodařilo se získat velikost souboru"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "URL LDAP serveru není definována",
|
||||
"bindFailed": "nepodařilo se připojit k výchozímu uživateli"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Tento účet je již propojen s jiným uživatelem",
|
||||
"not_linked": "Tento účet není propojen s žádným uživatelem",
|
||||
"no_user": "Pro tento účet nebyl nalezen žádný uživatel",
|
||||
"no_email": "Pro tento účet nebyla nalezena žádná emailová adresa",
|
||||
"unverified_account": "Váš účet není ověřen",
|
||||
"user_not_allowed": "Nemáte oprávnění se přihlásit",
|
||||
"invalid_token": "Neplatný token",
|
||||
"cannot_get_user_info": "Nepodařilo se získat informace o uživateli",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Token reverzního sdílení nenalezen",
|
||||
"maxShareSizeExceeded": "Maximální velikost sdílení nesmí být větší než {maxSize} bajtů."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Nedostatek volného místa na disku",
|
||||
"idInUse": "ID sdílení se již používá",
|
||||
"alreadyCompleted": "Sdílení již bylo dokončeno",
|
||||
"completionRequiresFile": "Pro dokončení sdílení potřebujete alespoň jeden soubor.",
|
||||
"notFound": "Sdílení nenalezeno",
|
||||
"anonymousNoDelete": "Anonymní sdílení nelze smazat",
|
||||
"anonymousNoExpire": "Platnost anonymních sdílení nemůže vypršet",
|
||||
"anonymousNoUpdate": "Anonymní sdílení nelze aktualizovat",
|
||||
"invalidExpiration": "Neplatné datum vypršení platnosti",
|
||||
"maxExpirationExceeded": "Datum vypršení platnosti překračuje maximální datum",
|
||||
"wrongPassword": "Nesprávné heslo",
|
||||
"tokenRequired": "Token sdílení je povinný",
|
||||
"privateShare": "K tomuto sdílení má přístup pouze tvůrce reverzního sdílení",
|
||||
"maxViewsExceeded": "Maximální počet zobrazení byl překročen"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Uživatelské jméno může obsahovat pouze písmena, čísla, tečky a podtržítka",
|
||||
"idPattern": "ID může obsahovat pouze písmena, čísla, podtržítka a pomlčky"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Registrering er ikke tilladt",
|
||||
"emailOrUsernameRequired": "E-mail eller brugernavn er påkrævet",
|
||||
"wrongCredentials": "Forkert e-mail eller adgangskode",
|
||||
"passwordSignInDisabled": "Adgangskode-logon er deaktiveret",
|
||||
"tokenInvalidOrExpired": "Token er ugyldig eller udløbet",
|
||||
"invalidPassword": "Ugyldig adgangskode",
|
||||
"invalidLoginToken": "Ugyldig login-token",
|
||||
"loginTokenExpired": "Login-token er udløbet",
|
||||
"totpNotEnabled": "TOTP er ikke aktiveret",
|
||||
"invalidCode": "Ugyldig kode",
|
||||
"totpAlreadyEnabled": "TOTP er allerede aktiveret",
|
||||
"totpNotInProgress": "TOTP er ikke i gang",
|
||||
"userNotFound": "Bruger ikke fundet",
|
||||
"cannotDeleteLastAdmin": "Kan ikke slette den sidste administratorbruger",
|
||||
"ldapResetPasswordNotAllowed": "Denne konto kan ikke nulstille sin adgangskode her. Kontakt din administrator.",
|
||||
"userAlreadyExists": "En bruger med dette {field} findes allerede"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Konfigurationsvariabel ikke fundet",
|
||||
"editNotAllowed": "Du må kun opdatere konfigurationsvariabler via config.yaml-filen",
|
||||
"invalidType": "Konfigurationsvariabel skal være af typen {type}",
|
||||
"shareIdLengthValidation": "Delings-ID skal være mellem 2 og 50",
|
||||
"zipCompressionLevelValidation": "Zip-komprimeringsniveau skal være mellem 0 og 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Nogen",
|
||||
"shareRecipientsDescFallback": "Ingen beskrivelse",
|
||||
"shareRecipientsExpiresNeverFallback": "i: aldrig",
|
||||
"smtpDisabled": "SMTP er deaktiveret",
|
||||
"emailServiceDisabled": "E-mail-tjeneste deaktiveret",
|
||||
"testSubject": "Test-e-mail",
|
||||
"testText": "Dette er en test-e-mail",
|
||||
"sendFailed": "Kunne ikke sende e-mail",
|
||||
"locale": "da-DK"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Ugyldigt ID-format",
|
||||
"notFound": "Fil ikke fundet",
|
||||
"passwordProtected": "Denne deling er adgangskodebeskyttet",
|
||||
"alreadyCompleted": "Deling er allerede fuldført",
|
||||
"notEnoughSpace": "Ikke nok plads på serveren",
|
||||
"unexpectedChunk": "Uventet chunk modtaget",
|
||||
"maxSizeExceeded": "Maksimal delingsstørrelse overskredet",
|
||||
"zipError": "Fejl ved oprettelse af ZIP-fil",
|
||||
"s3DeleteError": "Kunne ikke slette fil fra S3",
|
||||
"s3UploadInitError": "Kunne ikke initialisere multipart-upload.",
|
||||
"s3SessionNotFound": "Multipart-upload-session ikke fundet.",
|
||||
"s3UploadFailed": "Multipart-upload mislykkedes. Uploaden er blevet afbrudt.",
|
||||
"s3SizeError": "Kunne ikke hente filstørrelse"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "LDAP-server-URL er ikke defineret",
|
||||
"bindFailed": "kunne ikke binde til standardbruger"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Denne konto er allerede tilknyttet en anden bruger",
|
||||
"not_linked": "Denne konto er ikke tilknyttet nogen bruger",
|
||||
"no_user": "Ingen bruger fundet til denne konto",
|
||||
"no_email": "Ingen e-mailadresse fundet til denne konto",
|
||||
"unverified_account": "Din konto er ikke verificeret",
|
||||
"user_not_allowed": "Du har ikke tilladelse til at logge ind",
|
||||
"invalid_token": "Ugyldig token",
|
||||
"cannot_get_user_info": "Kunne ikke hente brugeroplysninger",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Token til omvendt deling blev ikke fundet",
|
||||
"maxShareSizeExceeded": "Maksimal delingsstørrelse kan ikke være større end {maxSize} bytes."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Ikke nok diskplads til rådighed",
|
||||
"idInUse": "Delings-id er allerede i brug",
|
||||
"alreadyCompleted": "Deling er allerede fuldført",
|
||||
"completionRequiresFile": "Du skal bruge mindst én fil i din deling for at fuldføre den.",
|
||||
"notFound": "Deling ikke fundet",
|
||||
"anonymousNoDelete": "Anonyme delinger kan ikke slettes",
|
||||
"anonymousNoExpire": "Anonyme delinger kan ikke udløbe",
|
||||
"anonymousNoUpdate": "Anonyme delinger kan ikke opdateres",
|
||||
"invalidExpiration": "Ugyldig udløbsdato",
|
||||
"maxExpirationExceeded": "Udløbsdato overstiger den maksimale udløbsdato",
|
||||
"wrongPassword": "Forkert adgangskode",
|
||||
"tokenRequired": "Delings-token påkrævet",
|
||||
"privateShare": "Kun skaberen af den omvendte deling har adgang til denne deling",
|
||||
"maxViewsExceeded": "Maksimalt antal visninger overskredet"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Brugernavn kan kun indeholde bogstaver, tal, prikker og understregninger",
|
||||
"idPattern": "ID kan kun indeholde bogstaver, tal, understregninger og bindestreger"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Registrierung ist nicht erlaubt",
|
||||
"emailOrUsernameRequired": "E-Mail oder Benutzername ist erforderlich",
|
||||
"wrongCredentials": "Falsche E-Mail oder Passwort",
|
||||
"passwordSignInDisabled": "Passwort-Anmeldung ist deaktiviert",
|
||||
"tokenInvalidOrExpired": "Token ungültig oder abgelaufen",
|
||||
"invalidPassword": "Ungültiges Passwort",
|
||||
"invalidLoginToken": "Ungültiges Login-Token",
|
||||
"loginTokenExpired": "Login-Token abgelaufen",
|
||||
"totpNotEnabled": "TOTP ist nicht aktiviert",
|
||||
"invalidCode": "Ungültiger Code",
|
||||
"totpAlreadyEnabled": "TOTP ist bereits aktiviert",
|
||||
"totpNotInProgress": "TOTP ist nicht in Bearbeitung",
|
||||
"userNotFound": "Benutzer nicht gefunden",
|
||||
"cannotDeleteLastAdmin": "Der letzte Administrator kann nicht gelöscht werden",
|
||||
"ldapResetPasswordNotAllowed": "Dieses Konto kann sein Passwort hier nicht zurücksetzen. Bitte kontaktieren Sie Ihren Administrator.",
|
||||
"userAlreadyExists": "Ein Benutzer mit diesem {field} existiert bereits"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Konfigurationsvariable nicht gefunden",
|
||||
"editNotAllowed": "Sie dürfen Konfigurationsvariablen nur über die Datei config.yaml aktualisieren",
|
||||
"invalidType": "Konfigurationsvariable muss vom Typ {type} sein",
|
||||
"shareIdLengthValidation": "Die Länge der Freigabe-ID muss zwischen 2 und 50 liegen",
|
||||
"zipCompressionLevelValidation": "Die ZIP-Kompressionsstufe muss zwischen 0 und 9 liegen"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Jemand",
|
||||
"shareRecipientsDescFallback": "Keine Beschreibung",
|
||||
"shareRecipientsExpiresNeverFallback": "in: nie",
|
||||
"smtpDisabled": "SMTP ist deaktiviert",
|
||||
"emailServiceDisabled": "E-Mail-Dienst deaktiviert",
|
||||
"testSubject": "Test-E-Mail",
|
||||
"testText": "Dies ist eine Test-E-Mail",
|
||||
"sendFailed": "E-Mail konnte nicht gesendet werden",
|
||||
"locale": "de-DE"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Ungültiges ID-Format",
|
||||
"notFound": "Datei nicht gefunden",
|
||||
"passwordProtected": "Diese Freigabe ist passwortgeschützt",
|
||||
"alreadyCompleted": "Freigabe ist bereits abgeschlossen",
|
||||
"notEnoughSpace": "Nicht genügend Speicherplatz auf dem Server",
|
||||
"unexpectedChunk": "Unerwarteter Chunk empfangen",
|
||||
"maxSizeExceeded": "Maximale Freigabegröße überschritten",
|
||||
"zipError": "Fehler beim Erstellen der ZIP-Datei",
|
||||
"s3DeleteError": "Datei konnte nicht von S3 gelöscht werden",
|
||||
"s3UploadInitError": "Fehler beim Initialisieren des Multipart-Uploads.",
|
||||
"s3SessionNotFound": "Multipart-Upload-Sitzung nicht gefunden.",
|
||||
"s3UploadFailed": "Multipart-Upload fehlgeschlagen. Der Upload wurde abgebrochen.",
|
||||
"s3SizeError": "Dateigröße konnte nicht ermittelt werden"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "LDAP-Server-URL ist nicht definiert",
|
||||
"bindFailed": "Fehler beim Binden an den Standardbenutzer"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Dieses Konto ist bereits mit einem anderen Benutzer verknüpft",
|
||||
"not_linked": "Dieses Konto ist mit keinem Benutzer verknüpft",
|
||||
"no_user": "Kein Benutzer für dieses Konto gefunden",
|
||||
"no_email": "Keine E-Mail-Adresse für dieses Konto gefunden",
|
||||
"unverified_account": "Ihr Konto ist nicht verifiziert",
|
||||
"user_not_allowed": "Sie dürfen sich nicht anmelden",
|
||||
"invalid_token": "Ungültiges Token",
|
||||
"cannot_get_user_info": "Benutzerinformationen konnten nicht abgerufen werden",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Reverse-Share-Token nicht gefunden",
|
||||
"maxShareSizeExceeded": "Die maximale Freigabegröße darf nicht größer als {maxSize} Bytes sein."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Nicht genügend Festplattenspeicher verfügbar",
|
||||
"idInUse": "Freigabe-ID bereits in Verwendung",
|
||||
"alreadyCompleted": "Freigabe bereits abgeschlossen",
|
||||
"completionRequiresFile": "Sie benötigen mindestens eine Datei in Ihrer Freigabe, um sie abzuschließen.",
|
||||
"notFound": "Freigabe nicht gefunden",
|
||||
"anonymousNoDelete": "Anonyme Freigaben können nicht gelöscht werden",
|
||||
"anonymousNoExpire": "Anonyme Freigaben können nicht ablaufen",
|
||||
"anonymousNoUpdate": "Anonyme Freigaben können nicht aktualisiert werden",
|
||||
"invalidExpiration": "Ungültiges Ablaufdatum",
|
||||
"maxExpirationExceeded": "Ablaufdatum überschreitet das maximale Ablaufdatum",
|
||||
"wrongPassword": "Falsches Passwort",
|
||||
"tokenRequired": "Freigabe-Token erforderlich",
|
||||
"privateShare": "Nur der Ersteller der Reverse-Freigabe kann auf diese Freigabe zugreifen",
|
||||
"maxViewsExceeded": "Maximale Anzahl an Aufrufen überschritten"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Benutzername darf nur Buchstaben, Zahlen, Punkte und Unterstriche enthalten",
|
||||
"idPattern": "ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Η εγγραφή δεν επιτρέπεται",
|
||||
"emailOrUsernameRequired": "Απαιτείται email ή όνομα χρήστη",
|
||||
"wrongCredentials": "Λάθος email ή κωδικός πρόσβασης",
|
||||
"passwordSignInDisabled": "Η σύνδεση με κωδικό πρόσβασης είναι απενεργοποιημένη",
|
||||
"tokenInvalidOrExpired": "Το διακριτικό είναι άκυρο ή έχει λήξει",
|
||||
"invalidPassword": "Άκυρος κωδικός πρόσβασης",
|
||||
"invalidLoginToken": "Άκυρο διακριτικό σύνδεσης",
|
||||
"loginTokenExpired": "Το διακριτικό σύνδεσης έληξε",
|
||||
"totpNotEnabled": "Το TOTP δεν είναι ενεργοποιημένο",
|
||||
"invalidCode": "Άκυρος κωδικός",
|
||||
"totpAlreadyEnabled": "Το TOTP είναι ήδη ενεργοποιημένο",
|
||||
"totpNotInProgress": "Το TOTP δεν είναι σε εξέλιξη",
|
||||
"userNotFound": "Ο χρήστης δεν βρέθηκε",
|
||||
"cannotDeleteLastAdmin": "Δεν είναι δυνατή η διαγραφή του τελευταίου διαχειριστή",
|
||||
"ldapResetPasswordNotAllowed": "Αυτός ο λογαριασμός δεν μπορεί να κάνει επαναφορά κωδικού πρόσβασης εδώ. Επικοινωνήστε με τον διαχειριστή σας.",
|
||||
"userAlreadyExists": "Ένας χρήστης με αυτό το {field} υπάρχει ήδη"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Η μεταβλητή διαμόρφωσης δεν βρέθηκε",
|
||||
"editNotAllowed": "Επιτρέπεται η ενημέρωση των μεταβλητών διαμόρφωσης μόνο μέσω του αρχείου config.yaml",
|
||||
"invalidType": "Η μεταβλητή διαμόρφωσης πρέπει να είναι τύπου {type}",
|
||||
"shareIdLengthValidation": "Το μήκος του ID κοινής χρήσης πρέπει να είναι μεταξύ 2 και 50",
|
||||
"zipCompressionLevelValidation": "Το επίπεδο συμπίεσης Zip πρέπει να είναι μεταξύ 0 και 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Κάποιος",
|
||||
"shareRecipientsDescFallback": "Χωρίς περιγραφή",
|
||||
"shareRecipientsExpiresNeverFallback": "σε: ποτέ",
|
||||
"smtpDisabled": "Το SMTP είναι απενεργοποιημένο",
|
||||
"emailServiceDisabled": "Η υπηρεσία email είναι απενεργοποιημένη",
|
||||
"testSubject": "Email δοκιμής",
|
||||
"testText": "Αυτό είναι ένα email δοκιμής",
|
||||
"sendFailed": "Αποτυχία αποστολής email",
|
||||
"locale": "el-GR"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Άκυρη μορφή ID",
|
||||
"notFound": "Το αρχείο δεν βρέθηκε",
|
||||
"passwordProtected": "Αυτή η κοινή χρήση προστατεύεται με κωδικό πρόσβασης",
|
||||
"alreadyCompleted": "Η κοινή χρήση έχει ήδη ολοκληρωθεί",
|
||||
"notEnoughSpace": "Δεν υπάρχει αρκετός χώρος στον διακομιστή",
|
||||
"unexpectedChunk": "Λήφθηκε μη αναμενόμενο κομμάτι",
|
||||
"maxSizeExceeded": "Υπέρβαση μέγιστου μεγέθους κοινής χρήσης",
|
||||
"zipError": "Σφάλμα κατά τη δημιουργία αρχείου ZIP",
|
||||
"s3DeleteError": "Δεν ήταν δυνατή η διαγραφή του αρχείου από το S3",
|
||||
"s3UploadInitError": "Αποτυχία προετοιμασίας μεταφόρτωσης πολλών μερών.",
|
||||
"s3SessionNotFound": "Η περίοδος λειτουργίας μεταφόρτωσης πολλών μερών δεν βρέθηκε.",
|
||||
"s3UploadFailed": "Η μεταφόρτωση πολλών μερών απέτυχε. Η μεταφόρτωση διακόπηκε.",
|
||||
"s3SizeError": "Δεν ήταν δυνατή η ανάκτηση του μεγέθους του αρχείου"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "Η διεύθυνση URL του διακομιστή LDAP δεν έχει οριστεί",
|
||||
"bindFailed": "αποτυχία σύνδεσης με τον προεπιλεγμένο χρήστη"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Αυτός ο λογαριασμός είναι ήδη συνδεδεμένος με άλλον χρήστη",
|
||||
"not_linked": "Αυτός ο λογαριασμός δεν είναι συνδεδεμένος με κανέναν χρήστη",
|
||||
"no_user": "Δεν βρέθηκε χρήστης για αυτόν τον λογαριασμό",
|
||||
"no_email": "Δεν βρέθηκε διεύθυνση email για αυτόν τον λογαριασμό",
|
||||
"unverified_account": "Ο λογαριασμός σας δεν είναι επαληθευμένος",
|
||||
"user_not_allowed": "Δεν σας επιτρέπεται η σύνδεση",
|
||||
"invalid_token": "Άκυρο διακριτικό",
|
||||
"cannot_get_user_info": "Δεν ήταν δυνατή η λήψη πληροφοριών χρήστη",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Το διακριτικό αντίστροφης κοινής χρήσης δεν βρέθηκε",
|
||||
"maxShareSizeExceeded": "Το μέγιστο μέγεθος κοινής χρήσης δεν μπορεί να είναι μεγαλύτερο από {maxSize} bytes."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Δεν υπάρχει αρκετός διαθέσιμος χώρος στο δίσκο",
|
||||
"idInUse": "Το ID κοινής χρήσης χρησιμοποιείται ήδη",
|
||||
"alreadyCompleted": "Η κοινή χρήση έχει ήδη ολοκληρωθεί",
|
||||
"completionRequiresFile": "Χρειάζεστε τουλάχιστον ένα αρχείο στην κοινή χρήση σας για να την ολοκληρώσετε.",
|
||||
"notFound": "Η κοινή χρήση δεν βρέθηκε",
|
||||
"anonymousNoDelete": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να διαγραφούν",
|
||||
"anonymousNoExpire": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να λήξουν",
|
||||
"anonymousNoUpdate": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να ενημερωθούν",
|
||||
"invalidExpiration": "Άκυρη ημερομηνία λήξης",
|
||||
"maxExpirationExceeded": "Η ημερομηνία λήξης υπερβαίνει τη μέγιστη ημερομηνία λήξης",
|
||||
"wrongPassword": "Λάθος κωδικός πρόσβασης",
|
||||
"tokenRequired": "Απαιτείται διακριτικό κοινής χρήσης",
|
||||
"privateShare": "Μόνο ο δημιουργός της αντίστροφης κοινής χρήσης μπορεί να έχει πρόσβαση σε αυτήν την κοινή χρήση",
|
||||
"maxViewsExceeded": "Υπέρβαση μέγιστων προβολών"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Το όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς, τελείες και κάτω παύλες",
|
||||
"idPattern": "Το ID μπορεί να περιέχει μόνο γράμματα, αριθμούς, κάτω παύλες και παύλες"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Registration is not allowed",
|
||||
"emailOrUsernameRequired": "Email or username is required",
|
||||
"wrongCredentials": "Wrong email or password",
|
||||
"passwordSignInDisabled": "Password sign in is disabled",
|
||||
"tokenInvalidOrExpired": "Token invalid or expired",
|
||||
"invalidPassword": "Invalid password",
|
||||
"invalidLoginToken": "Invalid login token",
|
||||
"loginTokenExpired": "Login token expired",
|
||||
"totpNotEnabled": "TOTP is not enabled",
|
||||
"invalidCode": "Invalid code",
|
||||
"totpAlreadyEnabled": "TOTP is already enabled",
|
||||
"totpNotInProgress": "TOTP is not in progress",
|
||||
"userNotFound": "User not found",
|
||||
"cannotDeleteLastAdmin": "Cannot delete the last admin user",
|
||||
"ldapResetPasswordNotAllowed": "This account can't reset its password here. Please contact your administrator.",
|
||||
"userAlreadyExists": "A user with this {field} already exists"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Config variable not found",
|
||||
"editNotAllowed": "You are only allowed to update config variables via the config.yaml file",
|
||||
"invalidType": "Config variable must be of type {type}",
|
||||
"shareIdLengthValidation": "Share ID length must be between 2 and 50",
|
||||
"zipCompressionLevelValidation": "Zip compression level must be between 0 and 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Someone",
|
||||
"shareRecipientsDescFallback": "No description",
|
||||
"shareRecipientsExpiresNeverFallback": "in: never",
|
||||
"smtpDisabled": "SMTP is disabled",
|
||||
"emailServiceDisabled": "Email service disabled",
|
||||
"testSubject": "Test email",
|
||||
"testText": "This is a test email",
|
||||
"sendFailed": "Failed to send email",
|
||||
"locale": "en-US"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Invalid ID format",
|
||||
"notFound": "File not found",
|
||||
"passwordProtected": "This share is password protected",
|
||||
"alreadyCompleted": "Share is already completed",
|
||||
"notEnoughSpace": "Not enough space on the server",
|
||||
"unexpectedChunk": "Unexpected chunk received",
|
||||
"maxSizeExceeded": "Max share size exceeded",
|
||||
"zipError": "Error creating ZIP file",
|
||||
"s3DeleteError": "Could not delete file from S3",
|
||||
"s3UploadInitError": "Failed to initialize multipart upload.",
|
||||
"s3SessionNotFound": "Multipart upload session not found.",
|
||||
"s3UploadFailed": "Multipart upload failed. The upload has been aborted.",
|
||||
"s3SizeError": "Could not retrieve file size"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "LDAP server URL is not defined",
|
||||
"bindFailed": "failed to bind to default user"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "This account is already linked to another user",
|
||||
"not_linked": "This account is not linked to any user",
|
||||
"no_user": "No user found for this account",
|
||||
"no_email": "No email address found for this account",
|
||||
"unverified_account": "Your account is not verified",
|
||||
"user_not_allowed": "You are not allowed to sign in",
|
||||
"invalid_token": "Invalid token",
|
||||
"cannot_get_user_info": "Could not get user information",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Reverse share token not found",
|
||||
"maxShareSizeExceeded": "Max share size can't be greater than {maxSize} bytes."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Not enough disk space available",
|
||||
"idInUse": "Share id already in use",
|
||||
"alreadyCompleted": "Share already completed",
|
||||
"completionRequiresFile": "You need at least one file in your share to complete it.",
|
||||
"notFound": "Share not found",
|
||||
"anonymousNoDelete": "Anonymous shares can't be deleted",
|
||||
"anonymousNoExpire": "Anonymous shares can't be expired",
|
||||
"anonymousNoUpdate": "Anonymous shares can't be updated",
|
||||
"invalidExpiration": "Invalid expiration date",
|
||||
"maxExpirationExceeded": "Expiration date exceeds maximum expiration date",
|
||||
"wrongPassword": "Wrong password",
|
||||
"tokenRequired": "Share token required",
|
||||
"privateShare": "Only reverse share creator can access this share",
|
||||
"maxViewsExceeded": "Maximum views exceeded"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Username can only contain letters, numbers, dots and underscores",
|
||||
"idPattern": "ID can only contain letters, numbers, underscores and hyphens"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "No se permite el registro",
|
||||
"emailOrUsernameRequired": "Se requiere correo electrónico o nombre de usuario",
|
||||
"wrongCredentials": "Correo electrónico o contraseña incorrectos",
|
||||
"passwordSignInDisabled": "El inicio de sesión con contraseña está desactivado",
|
||||
"tokenInvalidOrExpired": "Token inválido o expirado",
|
||||
"invalidPassword": "Contraseña inválida",
|
||||
"invalidLoginToken": "Token de inicio de sesión inválido",
|
||||
"loginTokenExpired": "Token de inicio de sesión expirado",
|
||||
"totpNotEnabled": "TOTP no está habilitado",
|
||||
"invalidCode": "Código inválido",
|
||||
"totpAlreadyEnabled": "TOTP ya está habilitado",
|
||||
"totpNotInProgress": "TOTP no está en curso",
|
||||
"userNotFound": "Usuario no encontrado",
|
||||
"cannotDeleteLastAdmin": "No se puede eliminar al último usuario administrador",
|
||||
"ldapResetPasswordNotAllowed": "Esta cuenta no puede restablecer su contraseña aquí. Por favor, póngase en contacto con su administrador.",
|
||||
"userAlreadyExists": "Ya existe un usuario con este {field}"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Variable de configuración no encontrada",
|
||||
"editNotAllowed": "Solo se permite actualizar las variables de configuración a través del archivo config.yaml",
|
||||
"invalidType": "La variable de configuración debe ser de tipo {type}",
|
||||
"shareIdLengthValidation": "La longitud del ID de compartido debe estar entre 2 y 50",
|
||||
"zipCompressionLevelValidation": "El nivel de compresión Zip debe estar entre 0 y 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Alguien",
|
||||
"shareRecipientsDescFallback": "Sin descripción",
|
||||
"shareRecipientsExpiresNeverFallback": "en: nunca",
|
||||
"smtpDisabled": "SMTP está desactivado",
|
||||
"emailServiceDisabled": "Servicio de correo electrónico desactivado",
|
||||
"testSubject": "Correo electrónico de prueba",
|
||||
"testText": "Este es un correo electrónico de prueba",
|
||||
"sendFailed": "Error al enviar el correo electrónico",
|
||||
"locale": "es-ES"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Formato de ID inválido",
|
||||
"notFound": "Archivo no encontrado",
|
||||
"passwordProtected": "Este compartido está protegido por contraseña",
|
||||
"alreadyCompleted": "El compartido ya se ha completado",
|
||||
"notEnoughSpace": "No hay suficiente espacio en el servidor",
|
||||
"unexpectedChunk": "Se ha recibido un fragmento inesperado",
|
||||
"maxSizeExceeded": "Se ha superado el tamaño máximo de compartido",
|
||||
"zipError": "Error al crear el archivo ZIP",
|
||||
"s3DeleteError": "No se pudo eliminar el archivo de S3",
|
||||
"s3UploadInitError": "Error al inicializar la carga multiparte.",
|
||||
"s3SessionNotFound": "Sesión de carga multiparte no encontrada.",
|
||||
"s3UploadFailed": "Error en la carga multiparte. La carga ha sido abortada.",
|
||||
"s3SizeError": "No se pudo obtener el tamaño del archivo"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "La URL del servidor LDAP no está definida",
|
||||
"bindFailed": "error al vincular al usuario predeterminado"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Esta cuenta ya está vinculada a otro usuario",
|
||||
"not_linked": "Esta cuenta no está vinculada a ningún usuario",
|
||||
"no_user": "No se ha encontrado ningún usuario para esta cuenta",
|
||||
"no_email": "No se ha encontrado ninguna dirección de correo electrónico para esta cuenta",
|
||||
"unverified_account": "Su cuenta no está verificada",
|
||||
"user_not_allowed": "No tiene permiso para iniciar sesión",
|
||||
"invalid_token": "Token inválido",
|
||||
"cannot_get_user_info": "No se pudo obtener la información del usuario",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Token de compartido inverso no encontrado",
|
||||
"maxShareSizeExceeded": "El tamaño máximo del compartido no puede ser superior a {maxSize} bytes."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "No hay suficiente espacio en disco disponible",
|
||||
"idInUse": "El ID de compartido ya está en uso",
|
||||
"alreadyCompleted": "El compartido ya se ha completado",
|
||||
"completionRequiresFile": "Necesitas al menos un archivo en tu compartido para completarlo.",
|
||||
"notFound": "Compartido no encontrado",
|
||||
"anonymousNoDelete": "Los compartidos anónimos no se pueden eliminar",
|
||||
"anonymousNoExpire": "Los compartidos anónimos no pueden expirar",
|
||||
"anonymousNoUpdate": "Los compartidos anónimos no se pueden actualizar",
|
||||
"invalidExpiration": "Fecha de expiración inválida",
|
||||
"maxExpirationExceeded": "La fecha de expiración supera la fecha de expiración máxima",
|
||||
"wrongPassword": "Contraseña incorrecta",
|
||||
"tokenRequired": "Se requiere token de compartido",
|
||||
"privateShare": "Solo el creador del compartido inverso puede acceder a este compartido",
|
||||
"maxViewsExceeded": "Se ha superado el número máximo de visualizaciones"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "El nombre de usuario solo puede contener letras, números, puntos y guiones bajos",
|
||||
"idPattern": "El ID solo puede contener letras, números, guiones bajos y guiones"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Registreerimine pole lubatud",
|
||||
"emailOrUsernameRequired": "E-post või kasutajanimi on kohustuslik",
|
||||
"wrongCredentials": "Vale e-post või parool",
|
||||
"passwordSignInDisabled": "Parooliga sisselogimine on keelatud",
|
||||
"tokenInvalidOrExpired": "Token on kehtetu või aegunud",
|
||||
"invalidPassword": "Vale parool",
|
||||
"invalidLoginToken": "Vale sisselogimistoken",
|
||||
"loginTokenExpired": "Sisselogimistoken on aegunud",
|
||||
"totpNotEnabled": "TOTP pole lubatud",
|
||||
"invalidCode": "Vale kood",
|
||||
"totpAlreadyEnabled": "TOTP on juba lubatud",
|
||||
"totpNotInProgress": "TOTP pole pooleli",
|
||||
"userNotFound": "Kasutajat ei leitud",
|
||||
"cannotDeleteLastAdmin": "Viimast administraatorit ei saa kustutada",
|
||||
"ldapResetPasswordNotAllowed": "Selle konto parooli ei saa siin lähtestada. Võtke ühendust administraatoriga.",
|
||||
"userAlreadyExists": "Kasutaja selle väljaga {field} on juba olemas"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Konfiguratsioonimuutujat ei leitud",
|
||||
"editNotAllowed": "Konfiguratsioonimuutujaid on lubatud uuendada ainult config.yaml faili kaudu",
|
||||
"invalidType": "Konfiguratsioonimuutuja peab olema tüüpi {type}",
|
||||
"shareIdLengthValidation": "Jagamise ID pikkus peab olema vahemikus 2 kuni 50",
|
||||
"zipCompressionLevelValidation": "Zip-pakkimise tase peab olema vahemikus 0 kuni 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Keegi",
|
||||
"shareRecipientsDescFallback": "Kirjeldus puudub",
|
||||
"shareRecipientsExpiresNeverFallback": "sisse: mitte kunagi",
|
||||
"smtpDisabled": "SMTP on keelatud",
|
||||
"emailServiceDisabled": "E-posti teenus on keelatud",
|
||||
"testSubject": "Test-e-kiri",
|
||||
"testText": "See on test-e-kiri",
|
||||
"sendFailed": "E-kirja saatmine ebaõnnestus",
|
||||
"locale": "et-EE"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Vigane ID formaat",
|
||||
"notFound": "Faili ei leitud",
|
||||
"passwordProtected": "See jagamine on parooliga kaitstud",
|
||||
"alreadyCompleted": "Jagamine on juba lõpetatud",
|
||||
"notEnoughSpace": "Serveris pole piisavalt ruumi",
|
||||
"unexpectedChunk": "Saadi ootamatu andmeplokk",
|
||||
"maxSizeExceeded": "Jagamise maksimaalne suurus on ületatud",
|
||||
"zipError": "Viga ZIP-faili loomisel",
|
||||
"s3DeleteError": "Faili kustutamine S3-st ebaõnnestus",
|
||||
"s3UploadInitError": "Multipart upload'i algatamine ebaõnnestus.",
|
||||
"s3SessionNotFound": "Multipart upload'i sessiooni ei leitud.",
|
||||
"s3UploadFailed": "Multipart upload ebaõnnestus. Üleslaadimine on katkestatud.",
|
||||
"s3SizeError": "Faili suurust ei õnnestunud tuvastada"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "LDAP serveri URL pole määratud",
|
||||
"bindFailed": "vaikekasutajaga ühendamine ebaõnnestus"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "See konto on juba seotud teise kasutajaga",
|
||||
"not_linked": "See konto pole seotud ühegi kasutajaga",
|
||||
"no_user": "Selle konto jaoks ei leitud kasutajat",
|
||||
"no_email": "Selle konto jaoks ei leitud e-posti aadressi",
|
||||
"unverified_account": "Teie konto pole kinnitatud",
|
||||
"user_not_allowed": "Teil pole lubatud sisse logida",
|
||||
"invalid_token": "Vigane token",
|
||||
"cannot_get_user_info": "Kasutajaandmete hankimine ebaõnnestus",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Pöörd-jagamise tokenit ei leitud",
|
||||
"maxShareSizeExceeded": "Maksimaalne jagamise suurus ei tohi olla suurem kui {maxSize} baiti."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Kettal pole piisavalt vaba ruumi",
|
||||
"idInUse": "Jagamise ID on juba kasutusel",
|
||||
"alreadyCompleted": "Jagamine on juba lõpetatud",
|
||||
"completionRequiresFile": "Jagamise lõpetamiseks peab selles olema vähemalt üks fail.",
|
||||
"notFound": "Jagamist ei leitud",
|
||||
"anonymousNoDelete": "Anonüümseid jagamisi ei saa kustutada",
|
||||
"anonymousNoExpire": "Anonüümsed jagamised ei saa aeguda",
|
||||
"anonymousNoUpdate": "Anonüümseid jagamisi ei saa uuendada",
|
||||
"invalidExpiration": "Vigane aegumiskuupäev",
|
||||
"maxExpirationExceeded": "Aegumiskuupäev ületab maksimaalset aegumiskuupäeva",
|
||||
"wrongPassword": "Vale parool",
|
||||
"tokenRequired": "Jagamise token on kohustuslik",
|
||||
"privateShare": "Sellele jagamisele pääseb ligi ainult pöörd-jagamise looja",
|
||||
"maxViewsExceeded": "Maksimaalne vaatamiste arv on täis"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Kasutajanimi võib sisaldada ainult tähti, numbreid, punkte ja alakriipse",
|
||||
"idPattern": "ID võib sisaldada ainult tähti, numbreid, alakriipse ja sidekriipse"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "Rekisteröityminen ei ole sallittua",
|
||||
"emailOrUsernameRequired": "Sähköposti tai käyttäjänimi vaaditaan",
|
||||
"wrongCredentials": "Väärä sähköposti tai salasana",
|
||||
"passwordSignInDisabled": "Salasanalla kirjautuminen on poistettu käytöstä",
|
||||
"tokenInvalidOrExpired": "Tunniste on virheellinen tai vanhentunut",
|
||||
"invalidPassword": "Virheellinen salasana",
|
||||
"invalidLoginToken": "Virheellinen kirjautumistunniste",
|
||||
"loginTokenExpired": "Kirjautumistunniste on vanhentunut",
|
||||
"totpNotEnabled": "TOTP ei ole käytössä",
|
||||
"invalidCode": "Virheellinen koodi",
|
||||
"totpAlreadyEnabled": "TOTP on jo käytössä",
|
||||
"totpNotInProgress": "TOTP ei ole käynnissä",
|
||||
"userNotFound": "Käyttäjää ei löytynyt",
|
||||
"cannotDeleteLastAdmin": "Viimeistä ylläpitäjää ei voi poistaa",
|
||||
"ldapResetPasswordNotAllowed": "Tämä tili ei voi vaihtaa salasanaansa täällä. Ota yhteyttä ylläpitäjään.",
|
||||
"userAlreadyExists": "Käyttäjä tällä kentällä {field} on jo olemassa"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Asetusmuuttujaa ei löytynyt",
|
||||
"editNotAllowed": "Asetusmuuttujia saa päivittää vain config.yaml-tiedoston kautta",
|
||||
"invalidType": "Asetusmuuttujan on oltava tyyppiä {type}",
|
||||
"shareIdLengthValidation": "Jaon ID:n pituuden on oltava välillä 2-50",
|
||||
"zipCompressionLevelValidation": "Zip-pakkaustason on oltava välillä 0-9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Joku",
|
||||
"shareRecipientsDescFallback": "Ei kuvausta",
|
||||
"shareRecipientsExpiresNeverFallback": "sisään: ei koskaan",
|
||||
"smtpDisabled": "SMTP on poistettu käytöstä",
|
||||
"emailServiceDisabled": "Sähköpostipalvelu on poistettu käytöstä",
|
||||
"testSubject": "Testisähköposti",
|
||||
"testText": "Tämä on testisähköposti",
|
||||
"sendFailed": "Sähköpostin lähettäminen epäonnistui",
|
||||
"locale": "fi-FI"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Virheellinen ID-muoto",
|
||||
"notFound": "Tiedostoa ei löytynyt",
|
||||
"passwordProtected": "Tämä jako on suojattu salasanalla",
|
||||
"alreadyCompleted": "Jako on jo valmis",
|
||||
"notEnoughSpace": "Palvelimella ei ole tarpeeksi tilaa",
|
||||
"unexpectedChunk": "Vastaanotettiin odottamaton lohko",
|
||||
"maxSizeExceeded": "Jaon enimmäiskoko ylittyi",
|
||||
"zipError": "Virhe luotaessa ZIP-tiedostoa",
|
||||
"s3DeleteError": "Tiedostoa ei voitu poistaa S3:sta",
|
||||
"s3UploadInitError": "Moniosaisen latauksen alustus epäonnistui.",
|
||||
"s3SessionNotFound": "Moniosaisen latauksen istuntoa ei löytynyt.",
|
||||
"s3UploadFailed": "Moniosainen lataus epäonnistui. Lataus on keskeytetty.",
|
||||
"s3SizeError": "Tiedoston kokoa ei voitu hakea"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "LDAP-palvelimen URL-osoitetta ei ole määritetty",
|
||||
"bindFailed": "oletuskäyttäjään yhdistäminen epäonnistui"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"already_linked": "Tämä tili on jo linkitetty toiseen käyttäjään",
|
||||
"not_linked": "Tätä tiliä ei ole linkitetty mihinkään käyttäjään",
|
||||
"no_user": "Tälle tilille ei löytynyt käyttäjää",
|
||||
"no_email": "Tälle tilille ei löytynyt sähköpostiosoitetta",
|
||||
"unverified_account": "Tiliäsi ei ole vahvistettu",
|
||||
"user_not_allowed": "Sinulla ei ole oikeutta kirjautua sisään",
|
||||
"invalid_token": "Virheellinen tunniste",
|
||||
"cannot_get_user_info": "Käyttäjätietoja ei voitu hakea",
|
||||
"provider_github": "GitHub",
|
||||
"provider_google": "Google",
|
||||
"provider_microsoft": "Microsoft",
|
||||
"provider_discord": "Discord",
|
||||
"provider_oidc": "OIDC"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"notFound": "Käänteisen jaon tunnistetta ei löytynyt",
|
||||
"maxShareSizeExceeded": "Jaon enimmäiskoko ei voi olla suurempi kuin {maxSize} tavua."
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"notEnoughSpace": "Levytilaa ei ole tarpeeksi vapaana",
|
||||
"idInUse": "Jaon ID on jo käytössä",
|
||||
"alreadyCompleted": "Jako on jo valmis",
|
||||
"completionRequiresFile": "Tarvitset vähintään yhden tiedoston jaossasi sen viimeistelemiseksi.",
|
||||
"notFound": "Jakoa ei löytynyt",
|
||||
"anonymousNoDelete": "Anonyymejä jakoja ei voi poistaa",
|
||||
"anonymousNoExpire": "Anonyymit jaot eivät voi vanhentua",
|
||||
"anonymousNoUpdate": "Anonyymejä jakoja ei voi päivittää",
|
||||
"invalidExpiration": "Virheellinen vanhentumispäivä",
|
||||
"maxExpirationExceeded": "Vanhentumispäivä ylittää enimmäisvanhentumispäivän",
|
||||
"wrongPassword": "Väärä salasana",
|
||||
"tokenRequired": "Jaon tunniste vaaditaan",
|
||||
"privateShare": "Vain käänteisen jaon luoja voi käyttää tätä jakoa",
|
||||
"maxViewsExceeded": "Katselukertojen enimmäismäärä ylittyi"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usernamePattern": "Käyttäjänimi voi sisältää vain kirjaimia, numeroita, pisteitä ja alaviivoja",
|
||||
"idPattern": "ID voi sisältää vain kirjaimia, numeroita, alaviivoja ja yhdysmerkkejä"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"registrationNotAllowed": "L'inscription n'est pas autorisée",
|
||||
"emailOrUsernameRequired": "L'email ou le nom d'utilisateur est requis",
|
||||
"wrongCredentials": "Email ou mot de passe incorrect",
|
||||
"passwordSignInDisabled": "La connexion par mot de passe est désactivée",
|
||||
"tokenInvalidOrExpired": "Jeton invalide ou expiré",
|
||||
"invalidPassword": "Mot de passe invalide",
|
||||
"invalidLoginToken": "Jeton de connexion invalide",
|
||||
"loginTokenExpired": "Jeton de connexion expiré",
|
||||
"totpNotEnabled": "Le TOTP n'est pas activé",
|
||||
"invalidCode": "Code invalide",
|
||||
"totpAlreadyEnabled": "Le TOTP est déjà activé",
|
||||
"totpNotInProgress": "Le TOTP n'est pas en cours",
|
||||
"userNotFound": "Utilisateur non trouvé",
|
||||
"cannotDeleteLastAdmin": "Impossible de supprimer le dernier administrateur",
|
||||
"ldapResetPasswordNotAllowed": "Ce compte ne peut pas réinitialiser son mot de passe ici. Veuillez contacter votre administrateur.",
|
||||
"userAlreadyExists": "Un utilisateur avec ce {field} existe déjà"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"redisUrlNotSet": "Redis URL is not set",
|
||||
"redisTimedOut": "Redis timed out",
|
||||
"redisConnectionFailed": "Redis connection failed",
|
||||
"redisUnexpectedResponse": "Unexpected response from Redis",
|
||||
"redisError": "Redis error",
|
||||
"variableNotFound": "Variable de configuration non trouvée",
|
||||
"editNotAllowed": "Vous n'êtes autorisé à mettre à jour les variables de configuration que via le fichier config.yaml",
|
||||
"invalidType": "La variable de configuration doit être de type {type}",
|
||||
"shareIdLengthValidation": "La longueur de l'ID de partage doit être comprise entre 2 et 50",
|
||||
"zipCompressionLevelValidation": "Le niveau de compression Zip doit être compris entre 0 et 9"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"shareRecipientsCreatorFallback": "Quelqu'un",
|
||||
"shareRecipientsDescFallback": "Pas de description",
|
||||
"shareRecipientsExpiresNeverFallback": "dans : jamais",
|
||||
"smtpDisabled": "SMTP est désactivé",
|
||||
"emailServiceDisabled": "Service d'email désactivé",
|
||||
"testSubject": "Email de test",
|
||||
"testText": "Ceci est un email de test",
|
||||
"sendFailed": "Échec de l'envoi de l'email",
|
||||
"locale": "fr-FR"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"invalidIdFormat": "Format d'ID invalide",
|
||||
"notFound": "Fichier non trouvé",
|
||||
"passwordProtected": "Ce partage est protégé par mot de passe",
|
||||
"alreadyCompleted": "Le partage est déjà terminé",
|
||||
"notEnoughSpace": "Espace insuffisant sur le serveur",
|
||||
"unexpectedChunk": "Chunk inattendu reçu",
|
||||
"maxSizeExceeded": "Taille maximale de partage dépassée",
|
||||
"zipError": "Erreur lors de la création du fichier ZIP",
|
||||
"s3DeleteError": "Impossible de supprimer le fichier de S3",
|
||||
"s3UploadInitError": "Échec de l'initialisation de l'upload multipart.",
|
||||
"s3SessionNotFound": "Session d'upload multipart non trouvée.",
|
||||
"s3UploadFailed": "Échec de l'upload multipart. L'upload a été abandonné.",
|
||||
"s3SizeError": "Impossible de récupérer la taille du fichier"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"urlNotDefined": "L'URL du serveur LDAP n'est pas définie",
|
||||
"bindFailed": "Échec de la liaison à l'utilisateur par défaut"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user