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:
smp46
2026-05-19 14:55:36 +01:00
committed by GitHub
parent 54abd9a122
commit 1d788f7db3
293 changed files with 3404 additions and 125 deletions
+3 -1
View File
@@ -3,6 +3,8 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@nestjs/swagger"]
"plugins": ["@nestjs/swagger"],
"deleteOutDir": true,
"assets": [{ "include": "i18n/**/*", "watchAssets": true }]
}
}
+221 -6
View File
@@ -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"
},
+1
View File
@@ -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",
+5
View File
@@ -34,6 +34,11 @@ export const configVariables = {
defaultValue: "3 months",
secret: false,
},
defaultLanguage: {
type: "string",
defaultValue: "en-US",
secret: false,
},
},
appearance: {
themePrimaryColor: {
+19
View File
@@ -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 {}
+3 -1
View File
@@ -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);
+12 -10
View File
@@ -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);
+17 -12
View File
@@ -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({
+13 -5
View File
@@ -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;
+38 -6
View File
@@ -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,
);
}
}
+27 -10
View File
@@ -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);
+7 -5
View File
@@ -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",
);
}
+9 -7
View File
@@ -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}`);
+11 -9
View File
@@ -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")));
}
});
}
+18
View File
@@ -0,0 +1,18 @@
{
"registrationNotAllowed": "التسجيل غير مسموح به",
"emailOrUsernameRequired": "البريد الإلكتروني أو اسم المستخدم مطلوب",
"wrongCredentials": "البريد الإلكتروني أو كلمة المرور غير صحيحة",
"passwordSignInDisabled": "تسجيل الدخول بكلمة المرور معطل",
"tokenInvalidOrExpired": "الرمز غير صالح أو منتهي الصلاحية",
"invalidPassword": "كلمة مرور غير صالحة",
"invalidLoginToken": "رمز تسجيل دخول غير صالح",
"loginTokenExpired": "انتهت صلاحية رمز تسجيل الدخول",
"totpNotEnabled": "TOTP غير مفعل",
"invalidCode": "رمز غير صالح",
"totpAlreadyEnabled": "TOTP مفعل بالفعل",
"totpNotInProgress": "TOTP ليس قيد التنفيذ",
"userNotFound": "المستخدم غير موجود",
"cannotDeleteLastAdmin": "لا يمكن حذف آخر مستخدم مسؤول",
"ldapResetPasswordNotAllowed": "لا يمكن لهذا الحساب إعادة تعيين كلمة المرور هنا. يرجى الاتصال بالمسؤول.",
"userAlreadyExists": "يوجد مستخدم بهذا {field} بالفعل"
}
+12
View File
@@ -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"
}
+11
View File
@@ -0,0 +1,11 @@
{
"shareRecipientsCreatorFallback": "شخص ما",
"shareRecipientsDescFallback": "لا يوجد وصف",
"shareRecipientsExpiresNeverFallback": "في: أبداً",
"smtpDisabled": "SMTP معطل",
"emailServiceDisabled": "خدمة البريد الإلكتروني معطلة",
"testSubject": "بريد إلكتروني تجريبي",
"testText": "هذا بريد إلكتروني تجريبي",
"sendFailed": "فشل إرسال البريد الإلكتروني",
"locale": "ar-EG"
}
+15
View File
@@ -0,0 +1,15 @@
{
"invalidIdFormat": "تنسيق المعرف غير صالح",
"notFound": "الملف غير موجود",
"passwordProtected": "هذه المشاركة محمية بكلمة مرور",
"alreadyCompleted": "المشاركة مكتملة بالفعل",
"notEnoughSpace": "لا توجد مساحة كافية على الخادم",
"unexpectedChunk": "تم استلام قطعة غير متوقعة",
"maxSizeExceeded": "تم تجاوز الحد الأقصى لحجم المشاركة",
"zipError": "خطأ في إنشاء ملف ZIP",
"s3DeleteError": "تعذر حذف الملف من S3",
"s3UploadInitError": "فشل بدء التحميل المتعدد الأجزاء.",
"s3SessionNotFound": "جلسة التحميل المتعدد الأجزاء غير موجودة.",
"s3UploadFailed": "فشل التحميل المتعدد الأجزاء. تم إلغاء التحميل.",
"s3SizeError": "تعذر استرداد حجم الملف"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "عنوان URL لخادم LDAP غير محدد",
"bindFailed": "فشل الارتباط بالمستخدم الافتراضي"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "رمز المشاركة العكسية غير موجود",
"maxShareSizeExceeded": "لا يمكن أن يكون الحد الأقصى لحجم المشاركة أكبر من {maxSize} بايت."
}
+16
View File
@@ -0,0 +1,16 @@
{
"notEnoughSpace": "لا توجد مساحة قرص كافية متاحة",
"idInUse": "معرف المشاركة قيد الاستخدام بالفعل",
"alreadyCompleted": "المشاركة مكتملة بالفعل",
"completionRequiresFile": "تحتاج إلى ملف واحد على الأقل في مشاركتك لإكمالها.",
"notFound": "المشاركة غير موجودة",
"anonymousNoDelete": "لا يمكن حذف المشاركات المجهولة",
"anonymousNoExpire": "لا يمكن أن تنتهي صلاحية المشاركات المجهولة",
"anonymousNoUpdate": "لا يمكن تحديث المشاركات المجهولة",
"invalidExpiration": "تاريخ انتهاء صلاحية غير صالح",
"maxExpirationExceeded": "تاريخ انتهاء الصلاحية يتجاوز الحد الأقصى لتاريخ انتهاء الصلاحية",
"wrongPassword": "كلمة مرور خاطئة",
"tokenRequired": "رمز المشاركة مطلوب",
"privateShare": "يمكن لمنشئ المشاركة العكسية فقط الوصول إلى هذه المشاركة",
"maxViewsExceeded": "تم تجاوز الحد الأقصى للمشاهدات"
}
+4
View File
@@ -0,0 +1,4 @@
{
"usernamePattern": "يمكن أن يحتوي اسم المستخدم فقط على أحرف وأرقام ونقاط وشرطات سفلية",
"idPattern": "يمكن أن يحتوي المعرف فقط على أحرف وأرقام وشرطات سفلية وشرطات"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "URL LDAP serveru není definována",
"bindFailed": "nepodařilo se připojit k výchozímu uživateli"
}
+15
View File
@@ -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"
}
+4
View File
@@ -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ů."
}
+16
View File
@@ -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"
}
+4
View File
@@ -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"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "LDAP-server-URL er ikke defineret",
"bindFailed": "kunne ikke binde til standardbruger"
}
+15
View File
@@ -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"
}
+4
View File
@@ -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."
}
+16
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"usernamePattern": "Brugernavn kan kun indeholde bogstaver, tal, prikker og understregninger",
"idPattern": "ID kan kun indeholde bogstaver, tal, understregninger og bindestreger"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "LDAP-Server-URL ist nicht definiert",
"bindFailed": "Fehler beim Binden an den Standardbenutzer"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "Reverse-Share-Token nicht gefunden",
"maxShareSizeExceeded": "Die maximale Freigabegröße darf nicht größer als {maxSize} Bytes sein."
}
+16
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"usernamePattern": "Benutzername darf nur Buchstaben, Zahlen, Punkte und Unterstriche enthalten",
"idPattern": "ID darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten"
}
+18
View File
@@ -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} υπάρχει ήδη"
}
+12
View File
@@ -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"
}
+11
View File
@@ -0,0 +1,11 @@
{
"shareRecipientsCreatorFallback": "Κάποιος",
"shareRecipientsDescFallback": "Χωρίς περιγραφή",
"shareRecipientsExpiresNeverFallback": "σε: ποτέ",
"smtpDisabled": "Το SMTP είναι απενεργοποιημένο",
"emailServiceDisabled": "Η υπηρεσία email είναι απενεργοποιημένη",
"testSubject": "Email δοκιμής",
"testText": "Αυτό είναι ένα email δοκιμής",
"sendFailed": "Αποτυχία αποστολής email",
"locale": "el-GR"
}
+15
View File
@@ -0,0 +1,15 @@
{
"invalidIdFormat": "Άκυρη μορφή ID",
"notFound": "Το αρχείο δεν βρέθηκε",
"passwordProtected": "Αυτή η κοινή χρήση προστατεύεται με κωδικό πρόσβασης",
"alreadyCompleted": "Η κοινή χρήση έχει ήδη ολοκληρωθεί",
"notEnoughSpace": "Δεν υπάρχει αρκετός χώρος στον διακομιστή",
"unexpectedChunk": "Λήφθηκε μη αναμενόμενο κομμάτι",
"maxSizeExceeded": "Υπέρβαση μέγιστου μεγέθους κοινής χρήσης",
"zipError": "Σφάλμα κατά τη δημιουργία αρχείου ZIP",
"s3DeleteError": "Δεν ήταν δυνατή η διαγραφή του αρχείου από το S3",
"s3UploadInitError": "Αποτυχία προετοιμασίας μεταφόρτωσης πολλών μερών.",
"s3SessionNotFound": "Η περίοδος λειτουργίας μεταφόρτωσης πολλών μερών δεν βρέθηκε.",
"s3UploadFailed": "Η μεταφόρτωση πολλών μερών απέτυχε. Η μεταφόρτωση διακόπηκε.",
"s3SizeError": "Δεν ήταν δυνατή η ανάκτηση του μεγέθους του αρχείου"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "Η διεύθυνση URL του διακομιστή LDAP δεν έχει οριστεί",
"bindFailed": "αποτυχία σύνδεσης με τον προεπιλεγμένο χρήστη"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "Το διακριτικό αντίστροφης κοινής χρήσης δεν βρέθηκε",
"maxShareSizeExceeded": "Το μέγιστο μέγεθος κοινής χρήσης δεν μπορεί να είναι μεγαλύτερο από {maxSize} bytes."
}
+16
View File
@@ -0,0 +1,16 @@
{
"notEnoughSpace": "Δεν υπάρχει αρκετός διαθέσιμος χώρος στο δίσκο",
"idInUse": "Το ID κοινής χρήσης χρησιμοποιείται ήδη",
"alreadyCompleted": "Η κοινή χρήση έχει ήδη ολοκληρωθεί",
"completionRequiresFile": "Χρειάζεστε τουλάχιστον ένα αρχείο στην κοινή χρήση σας για να την ολοκληρώσετε.",
"notFound": "Η κοινή χρήση δεν βρέθηκε",
"anonymousNoDelete": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να διαγραφούν",
"anonymousNoExpire": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να λήξουν",
"anonymousNoUpdate": "Οι ανώνυμες κοινές χρήσεις δεν μπορούν να ενημερωθούν",
"invalidExpiration": "Άκυρη ημερομηνία λήξης",
"maxExpirationExceeded": "Η ημερομηνία λήξης υπερβαίνει τη μέγιστη ημερομηνία λήξης",
"wrongPassword": "Λάθος κωδικός πρόσβασης",
"tokenRequired": "Απαιτείται διακριτικό κοινής χρήσης",
"privateShare": "Μόνο ο δημιουργός της αντίστροφης κοινής χρήσης μπορεί να έχει πρόσβαση σε αυτήν την κοινή χρήση",
"maxViewsExceeded": "Υπέρβαση μέγιστων προβολών"
}
+4
View File
@@ -0,0 +1,4 @@
{
"usernamePattern": "Το όνομα χρήστη μπορεί να περιέχει μόνο γράμματα, αριθμούς, τελείες και κάτω παύλες",
"idPattern": "Το ID μπορεί να περιέχει μόνο γράμματα, αριθμούς, κάτω παύλες και παύλες"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "LDAP server URL is not defined",
"bindFailed": "failed to bind to default user"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "Reverse share token not found",
"maxShareSizeExceeded": "Max share size can't be greater than {maxSize} bytes."
}
+16
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"usernamePattern": "Username can only contain letters, numbers, dots and underscores",
"idPattern": "ID can only contain letters, numbers, underscores and hyphens"
}
+18
View File
@@ -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}"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "La URL del servidor LDAP no está definida",
"bindFailed": "error al vincular al usuario predeterminado"
}
+15
View File
@@ -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"
}
+4
View File
@@ -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."
}
+16
View File
@@ -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"
}
+4
View File
@@ -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"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "LDAP serveri URL pole määratud",
"bindFailed": "vaikekasutajaga ühendamine ebaõnnestus"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "Pöörd-jagamise tokenit ei leitud",
"maxShareSizeExceeded": "Maksimaalne jagamise suurus ei tohi olla suurem kui {maxSize} baiti."
}
+16
View File
@@ -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"
}
+4
View File
@@ -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"
}
+18
View File
@@ -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"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"urlNotDefined": "LDAP-palvelimen URL-osoitetta ei ole määritetty",
"bindFailed": "oletuskäyttäjään yhdistäminen epäonnistui"
}
+15
View File
@@ -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"
}
+4
View File
@@ -0,0 +1,4 @@
{
"notFound": "Käänteisen jaon tunnistetta ei löytynyt",
"maxShareSizeExceeded": "Jaon enimmäiskoko ei voi olla suurempi kuin {maxSize} tavua."
}
+16
View File
@@ -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"
}
+4
View File
@@ -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ä"
}
+18
View File
@@ -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à"
}
+12
View File
@@ -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"
}
+11
View File
@@ -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"
}
+15
View File
@@ -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"
}
+4
View File
@@ -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