Compare commits

..

17 Commits

Author SHA1 Message Date
Elias Schneider
307d176430 release: 0.17.5 2023-09-03 22:14:34 +02:00
Elias Schneider
7e24ba9721 chore(translations): update translations via Crowdin (#238)
* New translations en-US.ts (French)

* New translations en-US.ts (Spanish)

* New translations en-US.ts (Danish)

* New translations en-US.ts (German)

* New translations en-US.ts (Finnish)

* New translations en-US.ts (Russian)

* New translations en-US.ts (Chinese Simplified)

* New translations en-US.ts (Portuguese, Brazilian)

* New translations en-US.ts (Thai)
2023-09-03 22:14:11 +02:00
Elias Schneider
f9774d82d8 refactor: run formatter 2023-09-03 22:13:57 +02:00
Elias Schneider
7647a9f620 fix: missing translation 2023-09-03 22:09:55 +02:00
Elias Schneider
d4e8d4f58b fix: autocomplete on create share modal 2023-09-03 22:07:40 +02:00
Elias Schneider
4df8dea5cc chore(translations): update translations via Crowdin (#232)
* New translations en-US.ts (Danish)

* New translations en-US.ts (French)

* New translations en-US.ts (Spanish)

* New translations en-US.ts (German)

* New translations en-US.ts (Finnish)

* New translations en-US.ts (Russian)

* New translations en-US.ts (Chinese Simplified)

* New translations en-US.ts (Portuguese, Brazilian)

* New translations en-US.ts (Thai)
2023-08-17 15:02:22 +02:00
Elias Schneider
84aa100f84 chore: formatter ignore translations 2023-08-17 15:00:57 +02:00
iUnstable0
bddb87b9b3 feat(localization): Added thai language (#231)
* feat(localization): Added Thai translation

* Formatted

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
2023-08-17 14:54:26 +02:00
Elias Schneider
18c10c0ac6 New translations en-US.ts (Danish) (#229) 2023-08-17 14:51:38 +02:00
Elias Schneider
f02e2979c4 refactor: run formatter 2023-08-17 14:47:58 +02:00
Elias Schneider
7b34cb14cb New translations en-US.ts (German) (#223) 2023-08-07 08:43:17 +02:00
Elias Schneider
019ef090ac chore(translations): update translations via Crowdin (#222)
* New translations en-US.ts (Portuguese, Brazilian)

* New translations en-US.ts (Portuguese, Brazilian)

* New translations en-US.ts (Russian)

* New translations en-US.ts (German)

* New translations en-US.ts (Russian)

* New translations en-US.ts (Finnish)

* New translations en-US.ts (Russian)
2023-08-01 12:50:41 +02:00
Elias Schneider
7304b54125 release: 0.17.4 2023-08-01 12:37:47 +02:00
Elias Schneider
ea0d5216e8 fix: redirection to localhost:3000 2023-08-01 12:35:37 +02:00
Elias Schneider
62deb6c152 release: 0.17.3 2023-07-31 16:38:58 +02:00
Elias Schneider
9ba2b4c82c fix: logo doesn't get loaded correctly 2023-07-31 16:38:29 +02:00
Elias Schneider
a47d080657 fix: share expiration never doesn't work if using another language than English 2023-07-31 16:34:24 +02:00
93 changed files with 905 additions and 649 deletions

View File

@@ -1 +0,0 @@
/backend/src/constants.ts

View File

@@ -1,3 +1,31 @@
## [0.17.5](https://github.com/stonith404/pingvin-share/compare/v0.17.4...v0.17.5) (2023-09-03)
### Features
* **localization:** Added thai language ([#231](https://github.com/stonith404/pingvin-share/issues/231)) ([bddb87b](https://github.com/stonith404/pingvin-share/commit/bddb87b9b3ec5426a3c7a14a96caf2eb45b93ff7))
### Bug Fixes
* autocomplete on create share modal ([d4e8d4f](https://github.com/stonith404/pingvin-share/commit/d4e8d4f58b9b7d10b865eff49aa784547891c4e8))
* missing translation ([7647a9f](https://github.com/stonith404/pingvin-share/commit/7647a9f620cbc5d38e019225a680a53bd3027698))
## [0.17.4](https://github.com/stonith404/pingvin-share/compare/v0.17.3...v0.17.4) (2023-08-01)
### Bug Fixes
* redirection to `localhost:3000` ([ea0d521](https://github.com/stonith404/pingvin-share/commit/ea0d5216e89346b8d3ef0277b76fdc6302e9de15))
## [0.17.3](https://github.com/stonith404/pingvin-share/compare/v0.17.2...v0.17.3) (2023-07-31)
### Bug Fixes
* logo doesn't get loaded correctly ([9ba2b4c](https://github.com/stonith404/pingvin-share/commit/9ba2b4c82cdad9097b33f0451771818c7b972a6b))
* share expiration never doesn't work if using another language than English ([a47d080](https://github.com/stonith404/pingvin-share/commit/a47d080657e1d08ef06ec7425d8bdafd5a26c24a))
## [0.17.2](https://github.com/stonith404/pingvin-share/compare/v0.17.1...v0.17.2) (2023-07-31)

View File

@@ -49,5 +49,5 @@ WORKDIR /opt/app
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
# HOSTNAME=127.0.0.1 fixes https://github.com/vercel/next.js/issues/51684. It can be removed as soon as the issue is fixed
CMD cp -rn /tmp/img /opt/app/frontend/public && HOSTNAME=127.0.0.1 node frontend/server.js & cd backend && npm run prod
# HOSTNAME=0.0.0.0 fixes https://github.com/vercel/next.js/issues/51684. It can be removed as soon as the issue is fixed
CMD cp -rn /tmp/img /opt/app/frontend/public && HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod

1
backend/.prettierignore Normal file
View File

@@ -0,0 +1 @@
/src/constants.ts

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-backend",
"version": "0.17.2",
"version": "0.17.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-backend",
"version": "0.17.2",
"version": "0.17.5",
"dependencies": {
"@nestjs/common": "^10.1.2",
"@nestjs/config": "^3.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share-backend",
"version": "0.17.2",
"version": "0.17.5",
"scripts": {
"build": "nest build",
"dev": "cross-env NODE_ENV=development nest start --watch",

View File

@@ -33,14 +33,14 @@ export class AuthController {
constructor(
private authService: AuthService,
private authTotpService: AuthTotpService,
private config: ConfigService
private config: ConfigService,
) {}
@Post("signUp")
@Throttle(10, 5 * 60)
async signUp(
@Body() dto: AuthRegisterDTO,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
if (!this.config.get("share.allowRegistration"))
throw new ForbiddenException("Registration is not allowed");
@@ -50,7 +50,7 @@ export class AuthController {
response = this.addTokensToResponse(
response,
result.refreshToken,
result.accessToken
result.accessToken,
);
return result;
@@ -61,7 +61,7 @@ export class AuthController {
@HttpCode(200)
async signIn(
@Body() dto: AuthSignInDTO,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
const result = await this.authService.signIn(dto);
@@ -69,7 +69,7 @@ export class AuthController {
response = this.addTokensToResponse(
response,
result.refreshToken,
result.accessToken
result.accessToken,
);
}
@@ -81,14 +81,14 @@ export class AuthController {
@HttpCode(200)
async signInTotp(
@Body() dto: AuthSignInTotpDTO,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
const result = await this.authTotpService.signInTotp(dto);
response = this.addTokensToResponse(
response,
result.refreshToken,
result.accessToken
result.accessToken,
);
return new TokenDTO().from(result);
@@ -113,12 +113,12 @@ export class AuthController {
async updatePassword(
@GetUser() user: User,
@Res({ passthrough: true }) response: Response,
@Body() dto: UpdatePasswordDTO
@Body() dto: UpdatePasswordDTO,
) {
const result = await this.authService.updatePassword(
user,
dto.oldPassword,
dto.password
dto.password,
);
response = this.addTokensToResponse(response, result.refreshToken);
@@ -129,12 +129,12 @@ export class AuthController {
@HttpCode(200)
async refreshAccessToken(
@Req() request: Request,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
if (!request.cookies.refresh_token) throw new UnauthorizedException();
const accessToken = await this.authService.refreshAccessToken(
request.cookies.refresh_token
request.cookies.refresh_token,
);
response = this.addTokensToResponse(response, undefined, accessToken);
return new TokenDTO().from({ accessToken });
@@ -143,7 +143,7 @@ export class AuthController {
@Post("signOut")
async signOut(
@Req() request: Request,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
await this.authService.signOut(request.cookies.access_token);
response.cookie("access_token", "accessToken", { maxAge: -1 });
@@ -176,7 +176,7 @@ export class AuthController {
private addTokensToResponse(
response: Response,
refreshToken?: string,
accessToken?: string
accessToken?: string,
) {
if (accessToken)
response.cookie("access_token", accessToken, { sameSite: "lax" });

View File

@@ -21,7 +21,7 @@ export class AuthService {
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService,
private emailService: EmailService
private emailService: EmailService,
) {}
async signUp(dto: AuthRegisterDTO) {
@@ -39,7 +39,7 @@ export class AuthService {
});
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
user.id
user.id,
);
const accessToken = await this.createAccessToken(user, refreshTokenId);
@@ -49,7 +49,7 @@ export class AuthService {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(
`A user with this ${duplicatedField} already exists`
`A user with this ${duplicatedField} already exists`,
);
}
}
@@ -78,7 +78,7 @@ export class AuthService {
}
const { refreshToken, refreshTokenId } = await this.createRefreshToken(
user.id
user.id,
);
const accessToken = await this.createAccessToken(user, refreshTokenId);
@@ -158,7 +158,7 @@ export class AuthService {
{
expiresIn: "15min",
secret: this.config.get("internal.jwtSecret"),
}
},
);
}
@@ -189,7 +189,7 @@ export class AuthService {
return this.createAccessToken(
refreshTokenMetaData.user,
refreshTokenMetaData.id
refreshTokenMetaData.id,
);
}

View File

@@ -18,7 +18,7 @@ export class AuthTotpService {
constructor(
private prisma: PrismaService,
private authService: AuthService,
private config: ConfigService
private config: ConfigService,
) {}
async signInTotp(dto: AuthSignInTotpDTO) {
@@ -72,7 +72,7 @@ export class AuthTotpService {
await this.authService.createRefreshToken(user.id);
const accessToken = await this.authService.createAccessToken(
user,
refreshTokenId
refreshTokenId,
);
return { accessToken, refreshToken };
@@ -98,7 +98,7 @@ export class AuthTotpService {
const otpURL = totp.keyuri(
user.username || user.email,
this.config.get("general.appName"),
secret
secret,
);
await this.prisma.user.update({

View File

@@ -5,5 +5,5 @@ export const GetUser = createParamDecorator(
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
}
},
);

View File

@@ -8,7 +8,10 @@ import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(config: ConfigService, private prisma: PrismaService) {
constructor(
config: ConfigService,
private prisma: PrismaService,
) {
config.get("internal.jwtSecret");
super({
jwtFromRequest: JwtStrategy.extractJWT,

View File

@@ -19,7 +19,7 @@ export class ClamScanService {
constructor(
private fileService: FileService,
private prisma: PrismaService
private prisma: PrismaService,
) {}
private ClamScan: Promise<NodeClam | null> = new NodeClam()
@@ -81,7 +81,7 @@ export class ClamScanService {
});
this.logger.warn(
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`
`Share ${shareId} deleted because it contained ${infectedFiles.length} malicious file(s)`,
);
}
}

View File

@@ -28,7 +28,7 @@ export class ConfigController {
constructor(
private configService: ConfigService,
private logoService: LogoService,
private emailService: EmailService
private emailService: EmailService,
) {}
@Get()
@@ -41,7 +41,7 @@ export class ConfigController {
@UseGuards(JwtGuard, AdministratorGuard)
async getByCategory(@Param("category") category: string) {
return new AdminConfigDTO().fromList(
await this.configService.getByCategory(category)
await this.configService.getByCategory(category),
);
}
@@ -49,7 +49,7 @@ export class ConfigController {
@UseGuards(JwtGuard, AdministratorGuard)
async updateMany(@Body() data: UpdateConfigDTO[]) {
return new AdminConfigDTO().fromList(
await this.configService.updateMany(data)
await this.configService.updateMany(data),
);
}
@@ -66,9 +66,9 @@ export class ConfigController {
@UploadedFile(
new ParseFilePipe({
validators: [new FileTypeValidator({ fileType: "image/png" })],
})
}),
)
file: Express.Multer.File
file: Express.Multer.File,
) {
return await this.logoService.create(file.buffer);
}

View File

@@ -11,12 +11,12 @@ import { PrismaService } from "src/prisma/prisma.service";
export class ConfigService {
constructor(
@Inject("CONFIG_VARIABLES") private configVariables: Config[],
private prisma: PrismaService
private prisma: PrismaService,
) {}
get(key: `${string}.${string}`): any {
const configVariable = this.configVariables.filter(
(variable) => `${variable.category}.${variable.name}` == key
(variable) => `${variable.category}.${variable.name}` == key,
)[0];
if (!configVariable) throw new Error(`Config variable ${key} not found`);
@@ -89,7 +89,7 @@ export class ConfigService {
configVariable.type != "text"
) {
throw new BadRequestException(
`Config variable must be of type ${configVariable.type}`
`Config variable must be of type ${configVariable.type}`,
);
}

View File

@@ -25,7 +25,7 @@ export class AdminConfigDTO extends ConfigDTO {
fromList(partial: Partial<AdminConfigDTO>[]) {
return partial.map((part) =>
plainToClass(AdminConfigDTO, part, { excludeExtraneousValues: true })
plainToClass(AdminConfigDTO, part, { excludeExtraneousValues: true }),
);
}
}

View File

@@ -12,7 +12,7 @@ export class ConfigDTO {
fromList(partial: Partial<ConfigDTO>[]) {
return partial.map((part) =>
plainToClass(ConfigDTO, part, { excludeExtraneousValues: true })
plainToClass(ConfigDTO, part, { excludeExtraneousValues: true }),
);
}
}

View File

@@ -25,7 +25,7 @@ export class LogoService {
fs.promises.writeFile(
`${IMAGES_PATH}/icons/icon-${size}x${size}.png`,
resized,
"binary"
"binary",
);
}
}

View File

@@ -32,7 +32,7 @@ export class EmailService {
await this.getTransporter()
.sendMail({
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
"smtp.email",
)}>`,
to: email,
subject,
@@ -49,7 +49,7 @@ export class EmailService {
shareId: string,
creator?: User,
description?: string,
expiration?: Date
expiration?: Date,
) {
if (!this.config.get("email.enableShareEmailRecipients"))
throw new InternalServerErrorException("Email service disabled");
@@ -69,8 +69,8 @@ export class EmailService {
"{expires}",
moment(expiration).unix() != 0
? moment(expiration).fromNow()
: "in: never"
)
: "in: never",
),
);
}
@@ -83,13 +83,13 @@ export class EmailService {
this.config
.get("email.reverseShareMessage")
.replaceAll("\\n", "\n")
.replaceAll("{shareUrl}", shareUrl)
.replaceAll("{shareUrl}", shareUrl),
);
}
async sendResetPasswordEmail(recipientEmail: string, token: string) {
const resetPasswordUrl = `${this.config.get(
"general.appUrl"
"general.appUrl",
)}/auth/resetPassword/${token}`;
await this.sendMail(
@@ -98,7 +98,7 @@ export class EmailService {
this.config
.get("email.resetPasswordMessage")
.replaceAll("\\n", "\n")
.replaceAll("{url}", resetPasswordUrl)
.replaceAll("{url}", resetPasswordUrl),
);
}
@@ -111,7 +111,7 @@ export class EmailService {
this.config
.get("email.inviteMessage")
.replaceAll("{url}", loginUrl)
.replaceAll("{password}", password)
.replaceAll("{password}", password),
);
}
@@ -119,7 +119,7 @@ export class EmailService {
await this.getTransporter()
.sendMail({
from: `"${this.config.get("general.appName")}" <${this.config.get(
"smtp.email"
"smtp.email",
)}>`,
to: recipientEmail,
subject: "Test email",

View File

@@ -28,7 +28,7 @@ export class FileController {
@Query() query: any,
@Body() body: string,
@Param("shareId") shareId: string
@Param("shareId") shareId: string,
) {
const { id, name, chunkIndex, totalChunks } = query;
@@ -39,7 +39,7 @@ export class FileController {
data,
{ index: parseInt(chunkIndex), total: parseInt(totalChunks) },
{ id, name },
shareId
shareId,
);
}
@@ -47,7 +47,7 @@ export class FileController {
@UseGuards(FileSecurityGuard)
async getZip(
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string
@Param("shareId") shareId: string,
) {
const zip = this.fileService.getZip(shareId);
res.set({
@@ -64,7 +64,7 @@ export class FileController {
@Res({ passthrough: true }) res: Response,
@Param("shareId") shareId: string,
@Param("fileId") fileId: string,
@Query("download") download = "true"
@Query("download") download = "true",
) {
const file = await this.fileService.get(shareId, fileId);

View File

@@ -18,14 +18,14 @@ export class FileService {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
private config: ConfigService
private config: ConfigService,
) {}
async create(
data: string,
chunk: { index: number; total: number },
file: { id?: string; name: string },
shareId: string
shareId: string,
) {
if (!file.id) file.id = crypto.randomUUID();
@@ -40,7 +40,7 @@ export class FileService {
let diskFileSize: number;
try {
diskFileSize = fs.statSync(
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
).size;
} catch {
diskFileSize = 0;
@@ -62,7 +62,7 @@ export class FileService {
// Check if share size limit is exceeded
const fileSizeSum = share.files.reduce(
(n, { size }) => n + parseInt(size),
0
0,
);
const shareSizeSum = fileSizeSum + diskFileSize + buffer.byteLength;
@@ -74,23 +74,23 @@ export class FileService {
) {
throw new HttpException(
"Max share size exceeded",
HttpStatus.PAYLOAD_TOO_LARGE
HttpStatus.PAYLOAD_TOO_LARGE,
);
}
fs.appendFileSync(
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
buffer
buffer,
);
const isLastChunk = chunk.index == chunk.total - 1;
if (isLastChunk) {
fs.renameSync(
`${SHARE_DIRECTORY}/${shareId}/${file.id}.tmp-chunk`,
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
`${SHARE_DIRECTORY}/${shareId}/${file.id}`,
);
const fileSize = fs.statSync(
`${SHARE_DIRECTORY}/${shareId}/${file.id}`
`${SHARE_DIRECTORY}/${shareId}/${file.id}`,
).size;
await this.prisma.file.create({
data: {

View File

@@ -14,7 +14,7 @@ import { ShareService } from "src/share/share.service";
export class FileSecurityGuard extends ShareSecurityGuard {
constructor(
private _shareService: ShareService,
private _prisma: PrismaService
private _prisma: PrismaService,
) {
super(_shareService, _prisma);
}
@@ -24,7 +24,7 @@ export class FileSecurityGuard extends ShareSecurityGuard {
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
"shareId",
)
? request.params.shareId
: request.params.id;
@@ -52,7 +52,7 @@ export class FileSecurityGuard extends ShareSecurityGuard {
if (share.security?.maxViews && share.security.maxViews <= share.views) {
throw new ForbiddenException(
"Maximum views exceeded",
"share_max_views_exceeded"
"share_max_views_exceeded",
);
}

View File

@@ -14,7 +14,7 @@ export class JobsService {
constructor(
private prisma: PrismaService,
private reverseShareService: ReverseShareService,
private fileService: FileService
private fileService: FileService,
) {}
@Cron("0 * * * *")
@@ -56,7 +56,7 @@ export class JobsService {
if (expiredReverseShares.length > 0) {
this.logger.log(
`Deleted ${expiredReverseShares.length} expired reverse shares`
`Deleted ${expiredReverseShares.length} expired reverse shares`,
);
}
}
@@ -77,7 +77,7 @@ export class JobsService {
for (const file of temporaryFiles) {
const stats = fs.statSync(
`${SHARE_DIRECTORY}/${shareDirectory}/${file}`
`${SHARE_DIRECTORY}/${shareDirectory}/${file}`,
);
const isOlderThanOneDay = moment(stats.mtime)
.add(1, "day")

View File

@@ -23,7 +23,7 @@ export class ReverseShareTokenWithShares extends OmitType(ReverseShareDTO, [
return partial.map((part) =>
plainToClass(ReverseShareTokenWithShares, part, {
excludeExtraneousValues: true,
})
}),
);
}
}

View File

@@ -23,7 +23,7 @@ import { ReverseShareService } from "./reverseShare.service";
export class ReverseShareController {
constructor(
private reverseShareService: ReverseShareService,
private config: ConfigService
private config: ConfigService,
) {}
@Post()
@@ -44,7 +44,7 @@ export class ReverseShareController {
if (!isValid) throw new NotFoundException("Reverse share token not found");
return new ReverseShareDTO().from(
await this.reverseShareService.getByToken(reverseShareToken)
await this.reverseShareService.getByToken(reverseShareToken),
);
}
@@ -52,7 +52,7 @@ export class ReverseShareController {
@UseGuards(JwtGuard)
async getAllByUser(@GetUser() user: User) {
return new ReverseShareTokenWithShares().fromList(
await this.reverseShareService.getAllByUser(user.id)
await this.reverseShareService.getAllByUser(user.id),
);
}

View File

@@ -10,7 +10,7 @@ export class ReverseShareService {
constructor(
private config: ConfigService,
private prisma: PrismaService,
private fileService: FileService
private fileService: FileService,
) {}
async create(data: CreateReverseShareDTO, creatorId: string) {
@@ -19,8 +19,8 @@ export class ReverseShareService {
.add(
data.shareExpiration.split("-")[0],
data.shareExpiration.split(
"-"
)[1] as moment.unitOfTime.DurationConstructor
"-",
)[1] as moment.unitOfTime.DurationConstructor,
)
.toDate();
@@ -28,7 +28,7 @@ export class ReverseShareService {
if (globalMaxShareSize < data.maxShareSize)
throw new BadRequestException(
`Max share size can't be greater than ${globalMaxShareSize} bytes.`
`Max share size can't be greater than ${globalMaxShareSize} bytes.`,
);
const reverseShare = await this.prisma.reverseShare.create({

View File

@@ -27,7 +27,7 @@ export class MyShareDTO extends OmitType(ShareDTO, [
fromList(partial: Partial<MyShareDTO>[]) {
return partial.map((part) =>
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true })
plainToClass(MyShareDTO, part, { excludeExtraneousValues: true }),
);
}
}

View File

@@ -29,7 +29,7 @@ export class ShareDTO {
fromList(partial: Partial<ShareDTO>[]) {
return partial.map((part) =>
plainToClass(ShareDTO, part, { excludeExtraneousValues: true })
plainToClass(ShareDTO, part, { excludeExtraneousValues: true }),
);
}
}

View File

@@ -7,7 +7,7 @@ import { ReverseShareService } from "src/reverseShare/reverseShare.service";
export class CreateShareGuard extends JwtGuard {
constructor(
configService: ConfigService,
private reverseShareService: ReverseShareService
private reverseShareService: ReverseShareService,
) {
super(configService);
}
@@ -21,7 +21,7 @@ export class CreateShareGuard extends JwtGuard {
if (!reverseShareTokenId) return false;
const isReverseShareTokenValid = await this.reverseShareService.isValid(
reverseShareTokenId
reverseShareTokenId,
);
return isReverseShareTokenValid;

View File

@@ -16,7 +16,7 @@ export class ShareOwnerGuard implements CanActivate {
const request: Request = context.switchToHttp().getRequest();
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
"shareId",
)
? request.params.shareId
: request.params.id;

View File

@@ -14,7 +14,7 @@ import { ShareService } from "src/share/share.service";
export class ShareSecurityGuard implements CanActivate {
constructor(
private shareService: ShareService,
private prisma: PrismaService
private prisma: PrismaService,
) {}
async canActivate(context: ExecutionContext) {
@@ -22,7 +22,7 @@ export class ShareSecurityGuard implements CanActivate {
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
"shareId",
)
? request.params.shareId
: request.params.id;
@@ -44,13 +44,13 @@ export class ShareSecurityGuard implements CanActivate {
if (share.security?.password && !shareToken)
throw new ForbiddenException(
"This share is password protected",
"share_password_required"
"share_password_required",
);
if (!(await this.shareService.verifyShareToken(shareId, shareToken)))
throw new ForbiddenException(
"Share token required",
"share_token_required"
"share_token_required",
);
return true;

View File

@@ -16,7 +16,7 @@ export class ShareTokenSecurity implements CanActivate {
const request: Request = context.switchToHttp().getRequest();
const shareId = Object.prototype.hasOwnProperty.call(
request.params,
"shareId"
"shareId",
)
? request.params.shareId
: request.params.id;

View File

@@ -33,7 +33,7 @@ export class ShareController {
@UseGuards(JwtGuard)
async getMyShares(@GetUser() user: User) {
return new MyShareDTO().fromList(
await this.shareService.getSharesByUser(user.id)
await this.shareService.getSharesByUser(user.id),
);
}
@@ -54,11 +54,11 @@ export class ShareController {
async create(
@Body() body: CreateShareDTO,
@Req() request: Request,
@GetUser() user: User
@GetUser() user: User,
) {
const { reverse_share_token } = request.cookies;
return new ShareDTO().from(
await this.shareService.create(body, user, reverse_share_token)
await this.shareService.create(body, user, reverse_share_token),
);
}
@@ -74,7 +74,7 @@ export class ShareController {
async complete(@Param("id") id: string, @Req() request: Request) {
const { reverse_share_token } = request.cookies;
return new ShareDTO().from(
await this.shareService.complete(id, reverse_share_token)
await this.shareService.complete(id, reverse_share_token),
);
}
@@ -91,7 +91,7 @@ export class ShareController {
async getShareToken(
@Param("id") id: string,
@Res({ passthrough: true }) response: Response,
@Body() body: SharePasswordDto
@Body() body: SharePasswordDto,
) {
const token = await this.shareService.getShareToken(id, body.password);
response.cookie(`share_${id}_token`, token, {

View File

@@ -28,7 +28,7 @@ export class ShareService {
private config: ConfigService,
private jwtService: JwtService,
private reverseShareService: ReverseShareService,
private clamScanService: ClamScanService
private clamScanService: ClamScanService,
) {}
async create(share: CreateShareDTO, user?: User, reverseShareToken?: string) {
@@ -46,7 +46,7 @@ export class ShareService {
// If share is created by a reverse share token override the expiration date
const reverseShare = await this.reverseShareService.getByToken(
reverseShareToken
reverseShareToken,
);
if (reverseShare) {
expirationDate = reverseShare.shareExpiration;
@@ -57,8 +57,8 @@ export class ShareService {
.add(
share.expiration.split("-")[0],
share.expiration.split(
"-"
)[1] as moment.unitOfTime.DurationConstructor
"-",
)[1] as moment.unitOfTime.DurationConstructor,
)
.toDate();
} else {
@@ -134,13 +134,13 @@ export class ShareService {
if (share.files.length == 0)
throw new BadRequestException(
"You need at least on file in your share to complete it."
"You need at least on file in your share to complete it.",
);
// Asynchronously create a zip of all files
if (share.files.length > 1)
this.createZip(id).then(() =>
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
this.prisma.share.update({ where: { id }, data: { isZipReady: true } }),
);
// Send email for each recipient
@@ -150,7 +150,7 @@ export class ShareService {
share.id,
share.creator,
share.description,
share.expiration
share.expiration,
);
}
@@ -161,7 +161,7 @@ export class ShareService {
) {
await this.emailService.sendMailToReverseShareCreator(
share.reverseShare.creator.email,
share.id
share.id,
);
}
@@ -285,7 +285,7 @@ export class ShareService {
if (share.security?.maxViews && share.security.maxViews <= share.views) {
throw new ForbiddenException(
"Maximum views exceeded",
"share_max_views_exceeded"
"share_max_views_exceeded",
);
}
@@ -305,7 +305,7 @@ export class ShareService {
{
expiresIn: moment(expiration).diff(new Date(), "seconds") + "s",
secret: this.config.get("internal.jwtSecret"),
}
},
);
}

View File

@@ -2,5 +2,5 @@ import { OmitType, PartialType } from "@nestjs/swagger";
import { UserDTO } from "./user.dto";
export class UpdateOwnUserDTO extends PartialType(
OmitType(UserDTO, ["isAdmin", "password"] as const)
OmitType(UserDTO, ["isAdmin", "password"] as const),
) {}

View File

@@ -31,7 +31,7 @@ export class UserDTO {
fromList(partial: Partial<UserDTO>[]) {
return partial.map((part) =>
plainToClass(UserDTO, part, { excludeExtraneousValues: true })
plainToClass(UserDTO, part, { excludeExtraneousValues: true }),
);
}
}

View File

@@ -35,7 +35,7 @@ export class UserController {
@UseGuards(JwtGuard)
async updateCurrentUser(
@GetUser() user: User,
@Body() data: UpdateOwnUserDTO
@Body() data: UpdateOwnUserDTO,
) {
return new UserDTO().from(await this.userService.update(user.id, data));
}
@@ -44,7 +44,7 @@ export class UserController {
@UseGuards(JwtGuard)
async deleteCurrentUser(
@GetUser() user: User,
@Res({ passthrough: true }) response: Response
@Res({ passthrough: true }) response: Response,
) {
response.cookie("access_token", "accessToken", { maxAge: -1 });
response.cookie("refresh_token", "", {

View File

@@ -11,7 +11,7 @@ import { UpdateUserDto } from "./dto/updateUser.dto";
export class UserSevice {
constructor(
private prisma: PrismaService,
private emailService: EmailService
private emailService: EmailService,
) {}
async list() {
@@ -46,7 +46,7 @@ export class UserSevice {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(
`A user with this ${duplicatedField} already exists`
`A user with this ${duplicatedField} already exists`,
);
}
}
@@ -66,7 +66,7 @@ export class UserSevice {
if (e.code == "P2002") {
const duplicatedField: string = e.meta.target[0];
throw new BadRequestException(
`A user with this ${duplicatedField} already exists`
`A user with this ${duplicatedField} already exists`,
);
}
}

1
frontend/.prettierignore Normal file
View File

@@ -0,0 +1 @@
/src/i18n/translations/*

View File

@@ -1,12 +1,12 @@
{
"name": "pingvin-share-frontend",
"version": "0.17.2",
"version": "0.17.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pingvin-share-frontend",
"version": "0.17.2",
"version": "0.17.5",
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",

View File

@@ -1,10 +1,10 @@
{
"name": "pingvin-share-frontend",
"version": "0.17.2",
"version": "0.17.5",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "node .next/standalone/server.js",
"start": "next start",
"lint": "next lint",
"format": "prettier --end-of-line=auto --write \"src/**/*.ts*\""
},

View File

@@ -7,7 +7,7 @@ import { LOCALES } from "../../i18n/locales";
const LanguagePicker = () => {
const t = useTranslate();
const [selectedLanguage, setSelectedLanguage] = useState(
getCookie("language")?.toString()
getCookie("language")?.toString(),
);
const languages = Object.values(LOCALES).map((locale) => ({
@@ -23,7 +23,7 @@ const LanguagePicker = () => {
setCookie("language", value, {
sameSite: "lax",
expires: new Date(
new Date().setFullYear(new Date().getFullYear() + 1)
new Date().setFullYear(new Date().getFullYear() + 1),
),
});
location.reload();

View File

@@ -14,7 +14,7 @@ import userPreferences from "../../utils/userPreferences.util";
const ThemeSwitcher = () => {
const [colorScheme, setColorScheme] = useState(
userPreferences.get("colorScheme")
userPreferences.get("colorScheme"),
);
const { toggleColorScheme } = useMantineColorScheme();
const systemColorScheme = useColorScheme();
@@ -26,7 +26,7 @@ const ThemeSwitcher = () => {
userPreferences.set("colorScheme", value);
setColorScheme(value);
toggleColorScheme(
value == "system" ? systemColorScheme : (value as ColorScheme)
value == "system" ? systemColorScheme : (value as ColorScheme),
);
}}
data={[

View File

@@ -26,7 +26,7 @@ const showEnableTotpModal = (
qrCode: string;
secret: string;
password: string;
}
},
) => {
const t = translateOutsideContext();
return modals.openModal({

View File

@@ -5,7 +5,7 @@ import { translateOutsideContext } from "../../hooks/useTranslate.hook";
const showReverseShareLinkModal = (
modals: ModalsContextProps,
reverseShareToken: string,
appUrl: string
appUrl: string,
) => {
const t = translateOutsideContext();
const link = `${appUrl}/upload/${reverseShareToken}`;

View File

@@ -12,7 +12,7 @@ const showShareInformationsModal = (
modals: ModalsContextProps,
share: MyShare,
appUrl: string,
maxShareSize: number
maxShareSize: number,
) => {
const t = translateOutsideContext();
const link = `${appUrl}/s/${share.id}`;

View File

@@ -5,7 +5,7 @@ import { translateOutsideContext } from "../../hooks/useTranslate.hook";
const showShareLinkModal = (
modals: ModalsContextProps,
shareId: string,
appUrl: string
appUrl: string,
) => {
const t = translateOutsideContext();
const link = `${appUrl}/s/${shareId}`;

View File

@@ -21,7 +21,7 @@ const AdminConfigInput = ({
stringValue: configVariable.value ?? configVariable.defaultValue,
textValue: configVariable.value ?? configVariable.defaultValue,
numberValue: parseInt(
configVariable.value ?? configVariable.defaultValue
configVariable.value ?? configVariable.defaultValue,
),
booleanValue:
(configVariable.value ?? configVariable.defaultValue) == "true",

View File

@@ -33,7 +33,7 @@ const TestEmailButton = ({
<Textarea minRows={4} readOnly value={e.response.data.message} />
</Stack>
),
})
}),
);
};

View File

@@ -17,7 +17,7 @@ import toast from "../../../utils/toast.util";
const showCreateUserModal = (
modals: ModalsContextProps,
smtpEnabled: boolean,
getUsers: () => void
getUsers: () => void,
) => {
return modals.openModal({
title: "Create user",
@@ -55,7 +55,7 @@ const Body = ({
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.optional(),
})
}),
),
});
@@ -87,7 +87,7 @@ const Body = ({
labelPosition="left"
label={t("admin.users.modal.create.manual-password")}
description={t(
"admin.users.modal.create.manual-password.description"
"admin.users.modal.create.manual-password.description",
)}
{...form.getInputProps("setPasswordManually", {
type: "checkbox",

View File

@@ -21,7 +21,7 @@ import toast from "../../../utils/toast.util";
const showUpdateUserModal = (
modals: ModalsContextProps,
user: User,
getUsers: () => void
getUsers: () => void,
) => {
const t = translateOutsideContext();
return modals.openModal({
@@ -53,7 +53,7 @@ const Body = ({
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
})
}),
),
});
@@ -66,7 +66,7 @@ const Body = ({
password: yup
.string()
.min(8, t("common.error.too-short", { length: 8 })),
})
}),
),
});
@@ -115,8 +115,8 @@ const Body = ({
})
.then(() =>
toast.success(
t("admin.users.edit.update.notify.password.success")
)
t("admin.users.edit.update.notify.password.success"),
),
)
.catch(toast.axiosError);
})}

View File

@@ -76,7 +76,7 @@ const SignUpForm = () => {
<Paper withBorder shadow="md" p={30} mt={30} radius="md">
<form
onSubmit={form.onSubmit((values) =>
signUp(values.email, values.username, values.password)
signUp(values.email, values.username, values.password),
)}
>
<TextInput

View File

@@ -44,6 +44,7 @@ const FilePreview = ({
href={`/api/shares/${shareId}/files/${fileId}?download=false`}
>
View original file
{/* Add translation? */}
</Button>
</Stack>
);

View File

@@ -8,7 +8,7 @@ import CopyTextField from "../../upload/CopyTextField";
const showCompletedReverseShareModal = (
modals: ModalsContextProps,
link: string,
getReverseShares: () => void
getReverseShares: () => void,
) => {
const t = translateOutsideContext();
return modals.openModal({

View File

@@ -13,7 +13,9 @@ import { useForm } from "@mantine/form";
import { useModals } from "@mantine/modals";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import { FormattedMessage } from "react-intl";
import useTranslate from "../../../hooks/useTranslate.hook";
import useTranslate, {
translateOutsideContext,
} from "../../../hooks/useTranslate.hook";
import shareService from "../../../services/share.service";
import { getExpirationPreview } from "../../../utils/date.util";
import toast from "../../../utils/toast.util";
@@ -23,10 +25,11 @@ import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
const showCreateReverseShareModal = (
modals: ModalsContextProps,
showSendEmailNotificationOption: boolean,
getReverseShares: () => void
getReverseShares: () => void,
) => {
const t = translateOutsideContext();
return modals.openModal({
title: "Create reverse share",
title: t("account.reverseShares.modal.title"),
children: (
<Body
showSendEmailNotificationOption={showSendEmailNotificationOption}
@@ -64,7 +67,7 @@ const Body = ({
values.expiration_num + values.expiration_unit,
values.maxShareSize,
values.maxUseCount,
values.sendEmailNotification
values.sendEmailNotification,
)
.then(({ link }) => {
modals.closeAll();
@@ -150,7 +153,7 @@ const Body = ({
expiresOn: t("account.reverseShare.expires-on"),
neverExpires: t("account.reverseShare.never-expires"),
},
form
form,
)}
</Text>
</div>
@@ -174,7 +177,7 @@ const Body = ({
labelPosition="left"
label={t("account.reverseShares.modal.send-email")}
description={t(
"account.reverseShares.modal.send-email.description"
"account.reverseShares.modal.send-email.description",
)}
{...form.getInputProps("sendEmailNotification", {
type: "checkbox",

View File

@@ -6,7 +6,7 @@ import FilePreview from "../FilePreview";
const showFilePreviewModal = (
shareId: string,
file: FileMetaData,
modals: ModalsContextProps
modals: ModalsContextProps,
) => {
const mimeType = (mime.contentType(file.name) || "").split(";")[0];
return modals.openModal({

View File

@@ -8,7 +8,7 @@ import useTranslate, {
const showEnterPasswordModal = (
modals: ModalsContextProps,
submitCallback: (password: string) => Promise<void>
submitCallback: (password: string) => Promise<void>,
) => {
const t = translateOutsideContext();
return modals.openModal({

View File

@@ -7,7 +7,7 @@ import { FormattedMessage } from "react-intl";
const showErrorModal = (
modals: ModalsContextProps,
title: string,
text: string
text: string,
) => {
return modals.openModal({
closeOnClickOutside: false,

View File

@@ -12,7 +12,7 @@ function CopyTextField(props: { link: string }) {
const [checkState, setCheckState] = useState(false);
const [textClicked, setTextClicked] = useState(false);
const timerRef = useRef<number | ReturnType<typeof setTimeout> | undefined>(
undefined
undefined,
);
const copyLink = () => {

View File

@@ -58,14 +58,14 @@ const Dropzone = ({
onDrop={(newFiles: FileUpload[]) => {
const fileSizeSum = [...newFiles, ...files].reduce(
(n, { size }) => n + size,
0
0,
);
if (fileSizeSum > maxShareSize) {
toast.error(
t("upload.dropzone.notify.file-too-big", {
maxSize: byteToHumanSizeString(maxShareSize),
})
}),
);
} else {
newFiles = newFiles.map((newFile) => {

View File

@@ -13,7 +13,7 @@ import CopyTextField from "../CopyTextField";
const showCompletedUploadModal = (
modals: ModalsContextProps,
share: Share,
appUrl: string
appUrl: string,
) => {
const t = translateOutsideContext();
return modals.openModal({

View File

@@ -38,7 +38,7 @@ const showCreateUploadModal = (
allowUnauthenticatedShares: boolean;
enableEmailRecepients: boolean;
},
uploadCallback: (createShare: CreateShare) => void
uploadCallback: (createShare: CreateShare) => void,
) => {
const t = translateOutsideContext();
@@ -119,7 +119,7 @@ const CreateUploadModalBody = ({
form.setFieldError("link", t("upload.modal.link.error.taken"));
} else {
const expiration = form.values.never_expires
? t("upload.modal.expires.never")
? "never"
: form.values.expiration_num + form.values.expiration_unit;
uploadCallback({
id: values.link,
@@ -140,7 +140,7 @@ const CreateUploadModalBody = ({
<TextInput
style={{ flex: "1" }}
variant="filled"
label="Link"
label={t("upload.modal.link.label")}
placeholder="myAwesomeShare"
{...form.getInputProps("link")}
/>
@@ -152,7 +152,7 @@ const CreateUploadModalBody = ({
"link",
Buffer.from(Math.random().toString(), "utf8")
.toString("base64")
.substr(10, 7)
.substr(10, 7),
)
}
>
@@ -251,7 +251,7 @@ const CreateUploadModalBody = ({
neverExpires: t("upload.modal.completed.never-expires"),
expiresOn: t("upload.modal.completed.expires-on"),
},
form
form,
)}
</Text>
</>
@@ -266,7 +266,7 @@ const CreateUploadModalBody = ({
<Textarea
variant="filled"
placeholder={t(
"upload.modal.accordion.description.placeholder"
"upload.modal.accordion.description.placeholder",
)}
{...form.getInputProps("description")}
/>
@@ -283,14 +283,14 @@ const CreateUploadModalBody = ({
data={form.values.recipients}
placeholder={t("upload.modal.accordion.email.placeholder")}
searchable
{...form.getInputProps("recipients")}
creatable
autoComplete="email-recipients"
getCreateLabel={(query) => `+ ${query}`}
onCreate={(query) => {
if (!query.match(/^\S+@\S+\.\S+$/)) {
form.setFieldError(
"recipients",
t("upload.modal.accordion.email.invalid-email")
t("upload.modal.accordion.email.invalid-email"),
);
} else {
form.setFieldError("recipients", null);
@@ -301,6 +301,7 @@ const CreateUploadModalBody = ({
return query;
}
}}
{...form.getInputProps("recipients")}
/>
</Accordion.Panel>
</Accordion.Item>
@@ -315,9 +316,10 @@ const CreateUploadModalBody = ({
<PasswordInput
variant="filled"
placeholder={t(
"upload.modal.accordion.security.password.placeholder"
"upload.modal.accordion.security.password.placeholder",
)}
label={t("upload.modal.accordion.security.password.label")}
autoComplete="off"
{...form.getInputProps("password")}
/>
<NumberInput
@@ -325,7 +327,7 @@ const CreateUploadModalBody = ({
type="number"
variant="filled"
placeholder={t(
"upload.modal.accordion.security.max-views.placeholder"
"upload.modal.accordion.security.max-views.placeholder",
)}
label={t("upload.modal.accordion.security.max-views.label")}
{...form.getInputProps("maxViews")}

View File

@@ -7,7 +7,7 @@ const useTranslate = () => {
return (
id: string,
values?: Parameters<typeof intl.formatMessage>[1],
opts?: Parameters<typeof intl.formatMessage>[2]
opts?: Parameters<typeof intl.formatMessage>[2],
) => {
return intl.formatMessage({ id }, values, opts) as string;
};
@@ -25,12 +25,12 @@ export const translateOutsideContext = () => {
messages: i18nUtil.getLocaleByCode(locale)?.messages,
defaultLocale: "en",
},
cache
cache,
);
return (
id: string,
values?: Parameters<typeof intl.formatMessage>[1],
opts?: Parameters<typeof intl.formatMessage>[2]
opts?: Parameters<typeof intl.formatMessage>[2],
) => {
return intl.formatMessage({ id }, values, opts) as string;
};

View File

@@ -7,6 +7,7 @@ import french from "./translations/fr-FR";
import portuguese from "./translations/pt-BR";
import russian from "./translations/ru-RU";
import chineseSimplified from "./translations/zh-CN";
import thai from "./translations/th-TH";
export const LOCALES = {
ENGLISH: {
@@ -54,4 +55,9 @@ export const LOCALES = {
code: "ru-RU",
messages: russian,
},
THAI: {
name: "ไทย",
code: "th-TH",
messages: thai,
},
};

View File

@@ -65,7 +65,7 @@ export default {
"account.notify.password.success": "Adgangskoden er ændret",
"account.card.security.title": "Sikkerhed",
"account.card.security.totp.enable.description": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
"account.card.security.totp.disable.description": "Enter your current password to disable TOTP",
"account.card.security.totp.disable.description": "Indtast din nuværende adgangskode for at begynde opsætningen af 2-faktor login",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Aktiver 2-faktor login",
"account.modal.totp.step1": "Trin 1: Tilføj din 2-faktor Authenticator",
@@ -102,7 +102,7 @@ export default {
"account.shares.table.size": "Størrelse",
"account.shares.modal.share-informations": "Share informations",
"account.shares.modal.share-link": "Del link",
"account.shares.modal.delete.title": "Delete share {share}",
"account.shares.modal.delete.title": "Slet share {share}",
"account.shares.modal.delete.description": "Ønsker du virkelig at slette denne deling?",
// END /account/shares
// /account/reverseShares
@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Der er tomt her 👀",
"account.reverseShares.description.empty": "You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Udløb",
"account.reverseShares.modal.expiration.minute-singular": "Minut",
"account.reverseShares.modal.expiration.minute-plural": "Minutter",
@@ -133,12 +134,12 @@ export default {
"account.reverseShare.expires-on": "Denne omvendte deling udløber den {expiration}.",
"account.reverseShares.table.no-shares": "Der er ikke oprettet nogle delinger endnu",
"account.reverseShares.table.count.singular": "del",
"account.reverseShares.table.count.plural": "delinger",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Delinger",
"account.reverseShares.table.remaining": "Resterende anvendelser",
"account.reverseShares.table.max-size": "Maksimal størrelse for deling",
"account.reverseShares.table.expires": "Udløber d",
"account.reverseShares.modal.reverse-share-link": "Omvendt deling",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title": "Delete reverse share",
"account.reverseShares.modal.delete.description": "Ønsker du virkelig at slette denne omvendte deling? Hvis du gør det, vil de tilknyttede delinger også blive slettet.",
// END /account/reverseShares
@@ -168,7 +169,7 @@ export default {
"admin.users.modal.create.password": "Adgangskode",
"admin.users.modal.create.manual-password": "Angiv adgangskode manuelt",
"admin.users.modal.create.manual-password.description": "If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin": "Admin rettigheder",
"admin.users.modal.create.admin.description": "If checked, the user will be able to access the admin panel.",
// END /admin/users
// /upload
@@ -205,7 +206,7 @@ export default {
"upload.modal.expires.year-singular": "År",
"upload.modal.expires.year-plural": "År",
"upload.modal.accordion.description.title": "Beskrivelse",
"upload.modal.accordion.description.placeholder": "Note for the recipients of this share",
"upload.modal.accordion.description.placeholder": "Bemærkning til modtagerne af dette share",
"upload.modal.accordion.email.title": "E-mail modtagere",
"upload.modal.accordion.email.placeholder": "Indtast e-mail modtagere",
"upload.modal.accordion.email.invalid-email": "Ugyldig e-mailadresse",
@@ -295,7 +296,7 @@ export default {
"admin.config.smtp.button.test": "Send test e-mail",
// 404
"404.description": "Ups! Denne side findes ikke.",
"404.button.home": "Bring me back home",
"404.button.home": "Gå tilbage",
// Common translations
"common.button.save": "Gem",
"common.button.create": "Opret",

View File

@@ -64,8 +64,8 @@ export default {
"account.card.password.new": "Neues Passwort",
"account.notify.password.success": "Passwort erfolgreich geändert",
"account.card.security.title": "Sicherheit",
"account.card.security.totp.enable.description": "Geben dein aktuelles Passwort ein, um TOTP zu aktivieren",
"account.card.security.totp.disable.description": "Geben dein aktuelles Passwort ein, um TOTP zu deaktivieren",
"account.card.security.totp.enable.description": "Gib dein aktuelles Passwort ein, um TOTP zu aktivieren",
"account.card.security.totp.disable.description": "Gib dein aktuelles Passwort ein, um TOTP zu deaktivieren",
"account.card.security.totp.button.start": "Starten",
"account.modal.totp.title": "TOTP aktivieren",
"account.modal.totp.step1": "Schritt 1: Füge deinen Authenticator hinzu",
@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Es ist leer hier 👀",
"account.reverseShares.description.empty": "Du hast keine externen Freigaben erstellt.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Gültig bis",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minuten",
@@ -177,7 +178,7 @@ export default {
"upload.notify.count-failed": "{count} Dateien konnten nicht hochgeladen werden. Wird erneut versucht.",
// Dropzone.tsx
"upload.dropzone.title": "Dateien hochladen",
"upload.dropzone.description": "Ziehen Sie Dateien hierher, um Ihre Freigabe zu starten. Wir können nur Dateien akzeptieren, die insgesamt weniger als {maxSize} groß sind.",
"upload.dropzone.description": "Ziehe Dateien hierher, um deine Freigabe zu starten. Wir können nur Dateien akzeptieren, die insgesamt weniger als {maxSize} groß sind.",
"upload.dropzone.notify.file-too-big": "Ihre Dateien überschreiten die maximale Freigabegröße von {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
@@ -228,7 +229,7 @@ export default {
"share.error.not-found.title": "Freigabe nicht gefunden",
"share.error.not-found.description": "Die gesuchte Freigabe existiert nicht.",
"share.modal.password.title": "Passwort erforderlich",
"share.modal.password.description": "Um auf diese Freigabe zuzugreifen, geben Sie bitte das Passwort für die Freigabe ein.",
"share.modal.password.description": "Um auf diese Freigabe zuzugreifen, gib bitte das Passwort für die Freigabe ein.",
"share.modal.password": "Passwort",
"share.modal.error.invalid-password": "Ungültiges Passwort",
"share.button.download-all": "Alles herunterladen",
@@ -237,7 +238,7 @@ export default {
"share.table.name": "Name",
"share.table.size": "Größe",
"share.modal.file-preview.error.not-supported.title": "Vorschau wird nicht unterstützt",
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte laden Sie die Datei herunter, um sie anzuzeigen.",
"share.modal.file-preview.error.not-supported.description": "Eine Vorschau für diesen Dateityp wird nicht unterstützt. Bitte lade die Datei herunter, um sie anzuzeigen.",
// END /share/[id]
// /admin/config
"admin.config.title": "Einstellungen",
@@ -279,7 +280,7 @@ export default {
"admin.config.share.max-size": "Maximale Größe",
"admin.config.share.max-size.description": "Maximale Größe einer Freigabe in Bytes",
"admin.config.share.zip-compression-level": "Zip Komprimierungsstufe",
"admin.config.share.zip-compression-level.description": "Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
"admin.config.share.zip-compression-level.description": "Passe den Wert an, um ein Gleichgewicht zwischen Dateigröße und Komprimierungsgeschwindigkeit herzustellen. Gültige Werte liegen zwischen 0 und 9, wobei 0 für keine Komprimierung und 9 für maximale Komprimierung steht.",
"admin.config.smtp.enabled": "Aktiviert",
"admin.config.smtp.enabled.description": "Gibt an, ob SMTP aktiviert ist. Aktiviere dies nur, wenn Du den Host, den Port, die Email, den Benutzernamen und das Passwort deines SMTP-Servers eingegeben hast.",
"admin.config.smtp.host": "Host",

View File

@@ -149,6 +149,7 @@ export default {
"You don't have any reverse shares.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",

View File

@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Aquí está vacío 👀",
"account.reverseShares.description.empty": "No tienes ningún Reverse Share.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Expiración",
"account.reverseShares.modal.expiration.minute-singular": "Minuto",
"account.reverseShares.modal.expiration.minute-plural": "Minutos",

View File

@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Täällä on tyhjää 👀",
"account.reverseShares.description.empty": "Sinulla ei ole käänteisiä jakoja.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Vanhentuminen",
"account.reverseShares.modal.expiration.minute-singular": "Minuutti",
"account.reverseShares.modal.expiration.minute-plural": "Minuuttia",
@@ -258,7 +259,7 @@ export default {
"admin.config.email.enable-share-email-recipients.description": "Salli sähköpostien jakaminen vastaanottajille. Ota tämä käyttöön vain, jos olet ottanut SMTP:n käyttöön.",
"admin.config.email.share-recipients-subject": "Sähköpostijaon otsikko",
"admin.config.email.share-recipients-subject.description": "Sähköpostin aihe, joka saa lähetetään jaon vastaanottajille.",
"admin.config.email.share-recipients-message": "Sähköpostijaon otsikko",
"admin.config.email.share-recipients-message": "Sähköpostijaon viesti",
"admin.config.email.share-recipients-message.description": "Viesti joka lähetetään jaon vastaanottajille. Saatavilla olevat muuttujat:\n {creator} - Jaon luojan käyttäjänimi\n {shareUrl} - Jaon URL\n {desc} - Jaon kuvaus\n {expires} - Jaon päättymispäivä\n Muuttujat korvataan todellisella arvolla.",
"admin.config.email.reverse-share-subject": "Käänteisen jaon aihe",
"admin.config.email.reverse-share-subject.description": "Aihe sähköpostin joka lähetetään kun joku loi jaon käänteisen jakolinkin kanssa.",

View File

@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Il n'y a rien ici 👀",
"account.reverseShares.description.empty": "Vous n'avez aucun partage inversé.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",

View File

@@ -26,7 +26,7 @@ export default {
"signin.title": "Bem-vindo de volta",
"signin.description": "Ainda não tem uma conta?",
"signin.button.signup": "Registo",
"signin.input.email-or-username": "Correio ou Nome de utilizador",
"signin.input.email-or-username": "E-mail ou nome de usuário",
"signin.input.email-or-username.placeholder": "Seu e-mail ou nome de usuário",
"signin.input.password": "Senha",
"signin.input.password.placeholder": "A sua senha",
@@ -38,7 +38,7 @@ export default {
"signup.title": "Criar uma conta",
"signup.description": "Já tem uma conta?",
"signup.button.signin": "Iniciar sessão",
"signup.input.username": "Utilizador",
"signup.input.username": "Nome de usuário",
"signup.input.username.placeholder": "Seu nome de usuário",
"signup.input.email": "E-mail",
"signup.input.email.placeholder": "Seu e-mail",
@@ -56,7 +56,7 @@ export default {
// /account
"account.title": "A minha conta",
"account.card.info.title": "Informação sobre a conta",
"account.card.info.username": "Nome do usuário",
"account.card.info.username": "Nome de usuário",
"account.card.info.email": "E-mail",
"account.notify.info.success": "A conta foi atualizada com sucesso",
"account.card.password.title": "Senha",
@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "Está vazio aqui 👀",
"account.reverseShares.description.empty": "Você não tem nenhum compartilhamento reverso.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Expiração",
"account.reverseShares.modal.expiration.minute-singular": "Minuto",
"account.reverseShares.modal.expiration.minute-plural": "Minutos",
@@ -150,7 +151,7 @@ export default {
// END /admin
// /admin/users
"admin.users.title": "Gerenciamento de usuários",
"admin.users.table.username": "Nome do usuário",
"admin.users.table.username": "Nome de usuário",
"admin.users.table.email": "E-mail",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Atualizar usuário {username}",
@@ -163,7 +164,7 @@ export default {
"admin.users.edit.delete.description": "Você realmente quer excluir este usuário e todos os seus compartilhamentos?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Criar usuário",
"admin.users.modal.create.username": "Nome do usuário",
"admin.users.modal.create.username": "Nome de usuário",
"admin.users.modal.create.email": "E-mail",
"admin.users.modal.create.password": "Senha",
"admin.users.modal.create.manual-password": "Definir senha manualmente",
@@ -281,14 +282,14 @@ export default {
"admin.config.share.zip-compression-level": "Nível de compressão",
"admin.config.share.zip-compression-level.description": "Ajuste o nível para equilibrar entre o tamanho do arquivo e a velocidade de compressão. Valores válidos vão de 0 a 9, com 0 sendo sem compressão e 9 sendo compressão máxima. ",
"admin.config.smtp.enabled": "Ativado",
"admin.config.smtp.enabled.description": "Se o SMTP está habilitado. Apenas defina como verdadeiro se você digitou o host, porta, e-mail, usuário e senha do seu servidor SMTP.",
"admin.config.smtp.enabled.description": "Se o SMTP está habilitado. Apenas defina como verdadeiro se você digitou o servidor, porta, e-mail, usuário e senha do seu servidor SMTP.",
"admin.config.smtp.host": "Servidor",
"admin.config.smtp.host.description": "Nome do Servidor SMTP",
"admin.config.smtp.port": "Porta",
"admin.config.smtp.port.description": "Porta do Servidor SMTP",
"admin.config.smtp.email": "E-mail",
"admin.config.smtp.email.description": "Endereço de e-mail do qual os e-mails são enviados",
"admin.config.smtp.username": "Nome do usuário",
"admin.config.smtp.username": "Nome de usuário",
"admin.config.smtp.username.description": "Nome de usuário do servidor SMTP",
"admin.config.smtp.password": "Senha",
"admin.config.smtp.password.description": "Senha do servidor SMTP",

View File

@@ -1,438 +1,324 @@
export default {
// Navbar
"navbar.upload": "Upload",
"navbar.signin": "Sign in",
"navbar.home": "Home",
"navbar.signup": "Sign Up",
"navbar.links.shares": "My shares",
"navbar.links.reverse": "Reverse shares",
"navbar.avatar.account": "My account",
"navbar.avatar.admin": "Administration",
"navbar.avatar.signout": "Sign out",
"navbar.upload": "Загрузить",
"navbar.signin": "Вход",
"navbar.home": "Главная",
"navbar.signup": "Зарегистрироваться",
"navbar.links.shares": "Мои загрузки",
"navbar.links.reverse": "Обратные загрузки",
"navbar.avatar.account": "Мой аккаунт",
"navbar.avatar.admin": "Администрирование",
"navbar.avatar.signout": "Выйти",
// END navbar
// /
"home.title": "A <h>self-hosted</h> file sharing platform.",
"home.description":
"Do you really want to give your personal files in the hand of third parties like WeTransfer?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "Host Pingvin Share on your own machine.",
"home.bullet.b.name": "Privacy",
"home.bullet.b.description":
"Your files are your files and should never get into the hands of third parties.",
"home.bullet.c.name": "No annoying file size limit",
"home.bullet.c.description":
"Upload as big files as you want. Only your hard drive will be your limit.",
"home.button.start": "Get started",
"home.button.source": "Source code",
"home.title": "Платформа для обмена файлами с <h>собственного хостинга</h>.",
"home.description": "Вы действительно хотите предоставить свои личные файлы в руки третьих лиц, таких как WeTransfer?",
"home.bullet.a.name": "На собственном сервере",
"home.bullet.a.description": "Pingvin Share работает на вашей машине.",
"home.bullet.b.name": "Конфиденциальность",
"home.bullet.b.description": "Ваши файлы - это ваши файлы и никогда не должны попадать в руки третьих лиц.",
"home.bullet.c.name": "Без раздражающего ограничения размера файла",
"home.bullet.c.description": "Загружайте файлы с любым размером. Только ваш жесткий диск будет пределом.",
"home.button.start": "Начнем",
"home.button.source": "Исходный код",
// END /
// /auth/signin
"signin.title": "Welcome back",
"signin.description": "You don't have an account yet?",
"signin.button.signup": "Sign up",
"signin.input.email-or-username": "Email or username",
"signin.input.email-or-username.placeholder": "Your email or username",
"signin.input.password": "Password",
"signin.input.password.placeholder": "Your password",
"signin.button.submit": "Sign in",
"signIn.notify.totp-required.title": "Two-factor authentication required",
"signIn.notify.totp-required.description":
"Please enter your two-factor authentication code",
"signin.title": "С возвращением",
"signin.description": "У вас еще нет учетной записи?",
"signin.button.signup": "Зарегистрироваться",
"signin.input.email-or-username": "Email или логин",
"signin.input.email-or-username.placeholder": "Эл. почта или логин",
"signin.input.password": "Пароль",
"signin.input.password.placeholder": "Ваш пароль",
"signin.button.submit": "Вход",
"signIn.notify.totp-required.title": "Требуется двухфакторная аутентификация",
"signIn.notify.totp-required.description": "Пожалуйста, введите код Вашей 2-х факторной аутентификации",
// END /auth/signin
// /auth/signup
"signup.title": "Create an account",
"signup.description": "Already have an account?",
"signup.button.signin": "Sign in",
"signup.input.username": "Username",
"signup.input.username.placeholder": "Your username",
"signup.input.email": "Email",
"signup.input.email.placeholder": "Your email",
"signup.button.submit": "Let's get started",
"signup.title": "Создать аккаунт",
"signup.description": "Уже есть учётная запись?",
"signup.button.signin": "Вход",
"signup.input.username": "Логин",
"signup.input.username.placeholder": "Ваш логин (имя пользователя)",
"signup.input.email": "Электронная почта",
"signup.input.email.placeholder": "Адрес эл. почты",
"signup.button.submit": "Давайте начнём",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "Forgot your password?",
"resetPassword.description": "Enter your email to reset your password.",
"resetPassword.notify.success":
"An email has been sent with a link to reset your password.",
"resetPassword.button.back": "Back to sign in page",
"resetPassword.text.resetPassword": "Reset password",
"resetPassword.text.enterNewPassword": "Enter your new password",
"resetPassword.input.password": "New password",
"resetPassword.notify.passwordReset":
"Your password has been reset successfully.",
"resetPassword.title": "Забыли пароль?",
"resetPassword.description": "Введите ваш email для восстановления пароля.",
"resetPassword.notify.success": "Вам направлено письмо со ссылкой для сброса пароля.",
"resetPassword.button.back": "Вернуться на страницу входа",
"resetPassword.text.resetPassword": "Сбросить пароль",
"resetPassword.text.enterNewPassword": "Введите новый пароль",
"resetPassword.input.password": "Новый пароль",
"resetPassword.notify.passwordReset": "Ваш пароль был успешно сброшен!",
// /account
"account.title": "My account",
"account.card.info.title": "Account info",
"account.card.info.username": "Username",
"account.card.info.email": "Email",
"account.notify.info.success": "Account updated successfully",
"account.card.password.title": "Password",
"account.card.password.old": "Old password",
"account.card.password.new": "New password",
"account.notify.password.success": "Password changed successfully",
"account.card.security.title": "Security",
"account.card.security.totp.enable.description":
"Enter your current password to start enabling TOTP",
"account.card.security.totp.disable.description":
"Enter your current password to disable TOTP",
"account.card.security.totp.button.start": "Start",
"account.modal.totp.title": "Enable TOTP",
"account.modal.totp.step1": "Step 1: Add your authenticator",
"account.modal.totp.step2": "Step 2: Validate your code",
"account.modal.totp.enterManually": "Enter manually",
"account.modal.totp.code": "Code",
"account.modal.totp.clickToCopy": "Click to copy",
"account.modal.totp.verify": "Verify",
"account.notify.totp.disable": "TOTP disabled successfully",
"account.notify.totp.enable": "TOTP enabled successfully",
"account.card.language.title": "Language",
"account.card.language.description":
"The project is translated by the community. Some languages might be incomplete.",
"account.card.color.title": "Color scheme",
"account.title": "Мой аккаунт",
"account.card.info.title": "Информация об аккаунте",
"account.card.info.username": "Логин",
"account.card.info.email": "Электронная почта",
"account.notify.info.success": "Настройки учетной записи успешно обновлены",
"account.card.password.title": "Пароль",
"account.card.password.old": "Старый пароль",
"account.card.password.new": "Новый пароль",
"account.notify.password.success": "Пароль успешно изменён",
"account.card.security.title": "Безопасность",
"account.card.security.totp.enable.description": "Введите ваш текущий пароль для начала включения TOTP",
"account.card.security.totp.disable.description": "Введите ваш текущий пароль, чтобы отключить TOTP",
"account.card.security.totp.button.start": "Начать",
"account.modal.totp.title": "Включить TOTP",
"account.modal.totp.step1": "Шаг 1: Добавьте свой аутентификатор",
"account.modal.totp.step2": "Шаг 2: Проверка кода",
"account.modal.totp.enterManually": "Ввести вручную",
"account.modal.totp.code": "Код",
"account.modal.totp.clickToCopy": "Нажмите, чтобы скопировать",
"account.modal.totp.verify": "Подтвердить",
"account.notify.totp.disable": "TOTP успешно отключен",
"account.notify.totp.enable": "TOTP успешно включен",
"account.card.language.title": "Язык",
"account.card.language.description": "Проект переведен сообществом. Некоторые языки могут быть неполными.",
"account.card.color.title": "Цветовая схема",
// ThemeSwitcher.tsx
"account.theme.dark": "Dark",
"account.theme.light": "Light",
"account.theme.system": "System",
"account.button.delete": "Delete Account",
"account.modal.delete.title": "Delete Account",
"account.modal.delete.description":
"Do you really want to delete your account including all your active shares?",
"account.theme.dark": "Тёмная",
"account.theme.light": "Светлая",
"account.theme.system": "Системная",
"account.button.delete": "Удалить аккаунт",
"account.modal.delete.title": "Удалить аккаунт",
"account.modal.delete.description": "Вы действительно хотите удалить свою учетную запись, включая все ваши загрузки?",
// END /account
// /account/shares
"account.shares.title": "My shares",
"account.shares.title.empty": "It's empty here 👀",
"account.shares.description.empty": "You don't have any shares.",
"account.shares.button.create": "Create one",
"account.shares.info.title": "Share informations",
"account.shares.title": "Мои загрузки",
"account.shares.title.empty": "Тут пусто 👀",
"account.shares.description.empty": "У вас нет загрузок.",
"account.shares.button.create": "Создать одну",
"account.shares.info.title": "Сведения",
"account.shares.table.id": "ID",
"account.shares.table.name": "Name",
"account.shares.table.description": "Description",
"account.shares.table.visitors": "Visitors",
"account.shares.table.expiresAt": "Expires at",
"account.shares.table.createdAt": "Created at",
"account.shares.table.size": "Size",
"account.shares.modal.share-informations": "Share informations",
"account.shares.modal.share-link": "Share link",
"account.shares.modal.delete.title": "Delete share {share}",
"account.shares.modal.delete.description":
"Do you really want to delete this share?",
"account.shares.table.name": "Название",
"account.shares.table.description": "Описание",
"account.shares.table.visitors": "Посетителей",
"account.shares.table.expiresAt": "Действительно до",
"account.shares.table.createdAt": "Создано",
"account.shares.table.size": "Размер",
"account.shares.modal.share-informations": "Сведения",
"account.shares.modal.share-link": "Поделиться ссылкой",
"account.shares.modal.delete.title": "Удалить загрузку {share}",
"account.shares.modal.delete.description": "Вы действительно хотите удалить эту загрузку?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "Reverse shares",
"account.reverseShares.description":
"A reverse share allows you to generate a unique URL that allows external users to create a share.",
"account.reverseShares.title.empty": "It's empty here 👀",
"account.reverseShares.description.empty":
"You don't have any reverse shares.",
"account.reverseShares.title": "Обратные загрузки",
"account.reverseShares.description": "Обратная загрузка позволяет генерировать уникальный URL, позволяющий внешним пользователям загружать файлы.",
"account.reverseShares.title.empty": "Тут пусто 👀",
"account.reverseShares.description.empty": "У вас пока нет обратных загрузок.",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.expiration.label": "Expiration",
"account.reverseShares.modal.expiration.minute-singular": "Minute",
"account.reverseShares.modal.expiration.minute-plural": "Minutes",
"account.reverseShares.modal.expiration.hour-singular": "Hour",
"account.reverseShares.modal.expiration.hour-plural": "Hours",
"account.reverseShares.modal.expiration.day-singular": "Day",
"account.reverseShares.modal.expiration.day-plural": "Days",
"account.reverseShares.modal.expiration.week-singular": "Week",
"account.reverseShares.modal.expiration.week-plural": "Weeks",
"account.reverseShares.modal.expiration.month-singular": "Month",
"account.reverseShares.modal.expiration.month-plural": "Months",
"account.reverseShares.modal.expiration.year-singular": "Year",
"account.reverseShares.modal.expiration.year-plural": "Years",
"account.reverseShares.modal.max-size.label": "Max share size",
"account.reverseShares.modal.send-email": "Send email notification",
"account.reverseShares.modal.send-email.description":
"Send an email notification when a share is created with this reverse share link.",
"account.reverseShares.modal.max-use.label": "Max uses",
"account.reverseShares.modal.max-use.description":
"The maximum amount of times this URL can be used to create a share.",
"account.reverseShare.never-expires": "This reverse share will never expire.",
"account.reverseShare.expires-on":
"This reverse share will expire on {expiration}.",
"account.reverseShares.table.no-shares": "No shares created yet",
"account.reverseShares.table.count.singular": "share",
"account.reverseShares.table.count.plural": "shares",
"account.reverseShares.table.shares": "Shares",
"account.reverseShares.table.remaining": "Remaining uses",
"account.reverseShares.table.max-size": "Max share size",
"account.reverseShares.table.expires": "Expires at",
"account.reverseShares.modal.reverse-share-link": "Reverse share link",
"account.reverseShares.modal.delete.title": "Delete reverse share",
"account.reverseShares.modal.delete.description":
"Do you really want to delete this reverse share? If you do, the associated shares will be deleted as well.",
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "Истекает",
"account.reverseShares.modal.expiration.minute-singular": "Минута",
"account.reverseShares.modal.expiration.minute-plural": "Минут(ы)",
"account.reverseShares.modal.expiration.hour-singular": "Час",
"account.reverseShares.modal.expiration.hour-plural": "Часов",
"account.reverseShares.modal.expiration.day-singular": "День",
"account.reverseShares.modal.expiration.day-plural": "Дней",
"account.reverseShares.modal.expiration.week-singular": "Неделя",
"account.reverseShares.modal.expiration.week-plural": "Недель",
"account.reverseShares.modal.expiration.month-singular": "Месяц",
"account.reverseShares.modal.expiration.month-plural": "Месяца(-ев)",
"account.reverseShares.modal.expiration.year-singular": "Год",
"account.reverseShares.modal.expiration.year-plural": "Года (лет)",
"account.reverseShares.modal.max-size.label": "Макс. размер загрузки",
"account.reverseShares.modal.send-email": "Отправить уведомление по эл. почте",
"account.reverseShares.modal.send-email.description": "Отправлять уведомление по электронной почте, когда загрузка создается с помощью этой обратной ссылки.",
"account.reverseShares.modal.max-use.label": "Максимум использований",
"account.reverseShares.modal.max-use.description": "Максимальное количество раз, когда URL может быть использован для создания загрузки.",
"account.reverseShare.never-expires": "Эта обратная загрузка никогда не устареет.",
"account.reverseShare.expires-on": "Эта обратная загрузка устареет {expiration}.",
"account.reverseShares.table.no-shares": "Нет созданных загрузок",
"account.reverseShares.table.count.singular": "загрузка",
"account.reverseShares.table.count.plural": "загрузки",
"account.reverseShares.table.shares": "Загрузки",
"account.reverseShares.table.remaining": "Осталось использований",
"account.reverseShares.table.max-size": "Макс. размер загрузки",
"account.reverseShares.table.expires": "Действительно до",
"account.reverseShares.modal.reverse-share-link": "Ссылка обратной загрузки",
"account.reverseShares.modal.delete.title": "Удалить обратную загрузку",
"account.reverseShares.modal.delete.description": "Вы действительно хотите удалить эту обратную загрузку? Если вы это сделаете, то все связанные обратные загрузки будут также удалены.",
// END /account/reverseShares
// /admin
"admin.title": "Administration",
"admin.button.users": "User management",
"admin.button.config": "Configuration",
"admin.version": "Version",
"admin.title": "Администрирование",
"admin.button.users": "Управление пользователями",
"admin.button.config": "Конфигурация",
"admin.version": "Версия",
// END /admin
// /admin/users
"admin.users.title": "User management",
"admin.users.table.username": "Username",
"admin.users.table.email": "Email",
"admin.users.table.admin": "Admin",
"admin.users.edit.update.title": "Update user {username}",
"admin.users.edit.update.admin-privileges": "Admin privileges",
"admin.users.edit.update.change-password.title": "Change password",
"admin.users.edit.update.change-password.field": "New password",
"admin.users.edit.update.change-password.button": "Save new password",
"admin.users.edit.update.notify.password.success":
"Password changed successfully",
"admin.users.edit.delete.title": "Delete user {username}",
"admin.users.edit.delete.description":
"Do you really want to delete this user and all his shares?",
"admin.users.title": "Управление пользователями",
"admin.users.table.username": "Логин",
"admin.users.table.email": "Электронная почта",
"admin.users.table.admin": "Администратор",
"admin.users.edit.update.title": "Обновить пользователя {username}",
"admin.users.edit.update.admin-privileges": "Права администратора",
"admin.users.edit.update.change-password.title": "Изменить пароль",
"admin.users.edit.update.change-password.field": "Новый пароль",
"admin.users.edit.update.change-password.button": "Сохранить новый пароль",
"admin.users.edit.update.notify.password.success": "Пароль успешно изменён",
"admin.users.edit.delete.title": "Удалить пользователя {username}",
"admin.users.edit.delete.description": "Вы действительно хотите удалить этого пользователя и все его загрузки?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "Create user",
"admin.users.modal.create.username": "Username",
"admin.users.modal.create.email": "Email",
"admin.users.modal.create.password": "Password",
"admin.users.modal.create.manual-password": "Set password manually",
"admin.users.modal.create.manual-password.description":
"If not checked, the user will receive an email with a link to set their password.",
"admin.users.modal.create.admin": "Admin privileges",
"admin.users.modal.create.admin.description":
"If checked, the user will be able to access the admin panel.",
"admin.users.modal.create.title": "Создать пользователя",
"admin.users.modal.create.username": "Логин",
"admin.users.modal.create.email": "Электронная почта",
"admin.users.modal.create.password": "Пароль",
"admin.users.modal.create.manual-password": "Установить пароль вручную",
"admin.users.modal.create.manual-password.description": "Если флажок не установлен, пользователь получит письмо со ссылкой для установки пароля.",
"admin.users.modal.create.admin": "Права администратора",
"admin.users.modal.create.admin.description": "Если отмечено, пользователь будет иметь доступ к панели администратора.",
// END /admin/users
// /upload
"upload.title": "Upload",
"upload.notify.generic-error":
"An error occurred while finishing your share.",
"upload.notify.count-failed": "{count} files failed to upload. Trying again.",
"upload.title": "Загрузить",
"upload.notify.generic-error": "Произошла ошибка при завершении вашей загрузки.",
"upload.notify.count-failed": "Не удалось загрузить файлы {count}. Повтор попытки.",
// Dropzone.tsx
"upload.dropzone.title": "Upload files",
"upload.dropzone.description":
"Drag'n'drop files here to start your share. We can accept only files that are less than {maxSize} in total.",
"upload.dropzone.notify.file-too-big":
"Your files exceed the maximum share size of {maxSize}.",
"upload.dropzone.title": "Загрузить файлы",
"upload.dropzone.description": "Перетащите сюда файлы для начала загрузки. Мы можем принимать только файлы, которые меньше {maxSize}.",
"upload.dropzone.notify.file-too-big": "Ваши файлы превышают максимальный размер в {maxSize}.",
// FileList.tsx
"upload.filelist.name": "Name",
"upload.filelist.size": "Size",
"upload.filelist.name": "Название",
"upload.filelist.size": "Размер",
// showCreateUploadModal.tsx
"upload.modal.title": "Create Share",
"upload.modal.link.error.invalid":
"Can only contain letters, numbers, underscores, and hyphens",
"upload.modal.link.error.taken": "This link is already in use",
"upload.modal.not-signed-in": "You're not signed in",
"upload.modal.not-signed-in-description":
"You will be unable to delete your share manually and view the visitor count.",
"upload.modal.expires.never": "never",
"upload.modal.expires.never-long": "Never Expires",
"upload.modal.link.label": "Link",
"upload.modal.expires.label": "Expiration",
"upload.modal.expires.minute-singular": "Minute",
"upload.modal.expires.minute-plural": "Minutes",
"upload.modal.expires.hour-singular": "Hour",
"upload.modal.expires.hour-plural": "Hours",
"upload.modal.expires.day-singular": "Day",
"upload.modal.expires.day-plural": "Days",
"upload.modal.expires.week-singular": "Week",
"upload.modal.expires.week-plural": "Weeks",
"upload.modal.expires.month-singular": "Month",
"upload.modal.expires.month-plural": "Months",
"upload.modal.expires.year-singular": "Year",
"upload.modal.expires.year-plural": "Years",
"upload.modal.accordion.description.title": "Description",
"upload.modal.accordion.description.placeholder":
"Note for the recipients of this share",
"upload.modal.accordion.email.title": "Email recipients",
"upload.modal.accordion.email.placeholder": "Enter email recipients",
"upload.modal.accordion.email.invalid-email": "Invalid email address",
"upload.modal.accordion.security.title": "Security options",
"upload.modal.accordion.security.password.label": "Password protection",
"upload.modal.accordion.security.password.placeholder": "No password",
"upload.modal.accordion.security.max-views.label": "Maximum views",
"upload.modal.accordion.security.max-views.placeholder": "No limit",
"upload.modal.title": "Загрузить",
"upload.modal.link.error.invalid": "Имя пользователя должно состоять только из букв, цифр, подчёркиваний и дефисов",
"upload.modal.link.error.taken": "Эта ссылка уже используется",
"upload.modal.not-signed-in": "Вы не авторизованы",
"upload.modal.not-signed-in-description": "Вы не сможете удалить свои файлы вручную и просмотреть количество посетителей.",
"upload.modal.expires.never": "никогда",
"upload.modal.expires.never-long": "Никогда не истекает",
"upload.modal.link.label": "Ссылка",
"upload.modal.expires.label": "Истекает",
"upload.modal.expires.minute-singular": "Минута",
"upload.modal.expires.minute-plural": "Минут(ы)",
"upload.modal.expires.hour-singular": "Час",
"upload.modal.expires.hour-plural": "Часов",
"upload.modal.expires.day-singular": "День",
"upload.modal.expires.day-plural": "Дней",
"upload.modal.expires.week-singular": "Неделя",
"upload.modal.expires.week-plural": "Недель",
"upload.modal.expires.month-singular": "Месяц",
"upload.modal.expires.month-plural": "Месяца(-ев)",
"upload.modal.expires.year-singular": "Год",
"upload.modal.expires.year-plural": "Года (лет)",
"upload.modal.accordion.description.title": "Описание",
"upload.modal.accordion.description.placeholder": "Примечание для получателей этой загрузки",
"upload.modal.accordion.email.title": "Получатели письма",
"upload.modal.accordion.email.placeholder": "Получатели e-mail",
"upload.modal.accordion.email.invalid-email": "Недопустимый адрес электронной почты",
"upload.modal.accordion.security.title": "Параметры безопасности",
"upload.modal.accordion.security.password.label": "Защита паролем",
"upload.modal.accordion.security.password.placeholder": "Без пароля",
"upload.modal.accordion.security.max-views.label": "Максимум просмотров",
"upload.modal.accordion.security.max-views.placeholder": "Без ограничения",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "This share will never expire.",
"upload.modal.completed.expires-on":
"This share will expire on {expiration}.",
"upload.modal.completed.share-ready": "Share ready",
"upload.modal.completed.never-expires": "Эта загрузка никогда не устареет.",
"upload.modal.completed.expires-on": "Эта загрузка устареет {expiration}.",
"upload.modal.completed.share-ready": "Готово",
// END /upload
// /share/[id]
"share.title": "Share {shareId}",
"share.description": "Look what I've shared with you!",
"share.error.visitor-limit-exceeded.title": "Visitor limit exceeded",
"share.error.visitor-limit-exceeded.description":
"The visitor limit from this share has been exceeded.",
"share.error.removed.title": "Share removed",
"share.error.not-found.title": "Share not found",
"share.error.not-found.description":
"The share you're looking for doesn't exist.",
"share.modal.password.title": "Password required",
"share.modal.password.description":
"To access this share please enter the password for the share.",
"share.modal.password": "Password",
"share.modal.error.invalid-password": "Invalid password",
"share.button.download-all": "Download all",
"share.notify.download-all-preparing":
"The share is preparing. Try again in a few minutes.",
"share.modal.file-link": "File link",
"share.table.name": "Name",
"share.table.size": "Size",
"share.modal.file-preview.error.not-supported.title": "Preview not supported",
"share.modal.file-preview.error.not-supported.description":
"A preview for thise file type is unsupported. Please download the file to view it.",
"share.title": "Загрузка {shareId}",
"share.description": "Посмотрите, чем я поделился с вами!",
"share.error.visitor-limit-exceeded.title": "Превышен лимит посетителей",
"share.error.visitor-limit-exceeded.description": "Превышен лимит посетителей.",
"share.error.removed.title": "Загрузка удалена",
"share.error.not-found.title": "Загрузка не найдена",
"share.error.not-found.description": "Страница, которую вы ищете, не существует.",
"share.modal.password.title": "Требуется пароль",
"share.modal.password.description": "Для доступа к этому ресурсу введите пароль для общего доступа.",
"share.modal.password": "Пароль",
"share.modal.error.invalid-password": "Неверный пароль",
"share.button.download-all": "Скачать все",
"share.notify.download-all-preparing": "Загрузка готовится. Повторите попытку через несколько минут.",
"share.modal.file-link": "Ссылка на файл",
"share.table.name": "Название",
"share.table.size": "Размер",
"share.modal.file-preview.error.not-supported.title": "Предпросмотр не поддерживается",
"share.modal.file-preview.error.not-supported.description": "Предварительный просмотр этого типа файла не поддерживается. Пожалуйста, скачайте файл для его просмотра.",
// END /share/[id]
// /admin/config
"admin.config.title": "Configuration",
"admin.config.category.general": "General",
"admin.config.category.share": "Share",
"admin.config.category.email": "Email",
"admin.config.title": "Конфигурация",
"admin.config.category.general": "Общее",
"admin.config.category.share": "Загрузки",
"admin.config.category.email": "Электронная почта",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "App name",
"admin.config.general.app-name.description": "Name of the application",
"admin.config.general.app-url": "App URL",
"admin.config.general.app-url.description":
"On which URL Pingvin Share is available",
"admin.config.general.show-home-page": "Show home page",
"admin.config.general.show-home-page.description":
"Whether to show the home page",
"admin.config.general.logo": "Logo",
"admin.config.general.logo.description":
"Change your logo by uploading a new image. The image must be a PNG and should have the format 1:1.",
"admin.config.general.logo.placeholder": "Pick image",
"admin.config.email.enable-share-email-recipients":
"Enable share email recipients",
"admin.config.email.enable-share-email-recipients.description":
"Whether to allow emails to share recipients. Only enable this if you have enabled SMTP.",
"admin.config.email.share-recipients-subject": "Share recipients subject",
"admin.config.email.share-recipients-subject.description":
"Subject of the email which gets sent to the share recipients.",
"admin.config.email.share-recipients-message": "Share recipients message",
"admin.config.email.share-recipients-message.description":
"Message which gets sent to the share recipients. Available variables:\n {creator} - The username of the creator of the share\n {shareUrl} - The URL of the share\n {desc} - The description of the share\n {expires} - The expiration date of the share\n The variables will be replaced with the actual value.",
"admin.config.email.reverse-share-subject": "Reverse share subject",
"admin.config.email.reverse-share-subject.description":
"Subject of the email which gets sent when someone created a share with your reverse share link.",
"admin.config.email.reverse-share-message": "Reverse share message",
"admin.config.email.reverse-share-message.description":
"Message which gets sent when someone created a share with your reverse share link. {shareUrl} will be replaced with the creator's name and the share URL.",
"admin.config.email.reset-password-subject": "Reset password subject",
"admin.config.email.reset-password-subject.description":
"Subject of the email which gets sent when a user requests a password reset.",
"admin.config.email.reset-password-message": "Reset password message",
"admin.config.email.reset-password-message.description":
"Message which gets sent when a user requests a password reset. {url} will be replaced with the reset password URL.",
"admin.config.email.invite-subject": "Invite subject",
"admin.config.email.invite-subject.description":
"Subject of the email which gets sent when an admin invites a user.",
"admin.config.email.invite-message": "Invite message",
"admin.config.email.invite-message.description":
"Message which gets sent when an admin invites a user. {url} will be replaced with the invite URL and {password} with the password.",
"admin.config.share.allow-registration": "Allow registration",
"admin.config.share.allow-registration.description":
"Whether registration is allowed",
"admin.config.share.allow-unauthenticated-shares":
"Allow unauthenticated shares",
"admin.config.share.allow-unauthenticated-shares.description":
"Whether unauthenticated users can create shares",
"admin.config.share.max-size": "Max size",
"admin.config.share.max-size.description": "Maximum share size in bytes",
"admin.config.share.zip-compression-level": "Zip compression level",
"admin.config.share.zip-compression-level.description":
"Adjust the level to balance between file size and compression speed. Valid values range from 0 to 9, with 0 being no compression and 9 being maximum compression. ",
"admin.config.smtp.enabled": "Enabled",
"admin.config.smtp.enabled.description":
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
"admin.config.smtp.host": "Host",
"admin.config.smtp.host.description": "Host of the SMTP server",
"admin.config.smtp.port": "Port",
"admin.config.smtp.port.description": "Port of the SMTP server",
"admin.config.smtp.email": "Email",
"admin.config.smtp.email.description":
"Email address which the emails get sent from",
"admin.config.smtp.username": "Username",
"admin.config.smtp.username.description": "Username of the SMTP server",
"admin.config.smtp.password": "Password",
"admin.config.smtp.password.description": "Password of the SMTP server",
"admin.config.smtp.button.test": "Send test email",
"admin.config.general.app-name": "Название приложения",
"admin.config.general.app-name.description": "Видимое название приложения",
"admin.config.general.app-url": "URL-адрес приложения",
"admin.config.general.app-url.description": "Адрес на котором доступен Pingvin Share",
"admin.config.general.show-home-page": "Показывать домашнюю страницу",
"admin.config.general.show-home-page.description": "Показывать ли домашнюю страницу или нет",
"admin.config.general.logo": "Логотип",
"admin.config.general.logo.description": "Измените свой логотип, загрузив новое изображение. Изображение должно быть PNG и должно иметь формат 1:1.",
"admin.config.general.logo.placeholder": "Выберите изображение",
"admin.config.email.enable-share-email-recipients": "Включить обмен с получателями электронной почты",
"admin.config.email.enable-share-email-recipients.description": "Разрешить ли отправку писем получателям. Включите, только если вы включили SMTP.",
"admin.config.email.share-recipients-subject": "Заголовок письма (загрузка)",
"admin.config.email.share-recipients-subject.description": "Тема письма, которое отправляется получателям акции.",
"admin.config.email.share-recipients-message": "Сообщение письма загрузки",
"admin.config.email.share-recipients-message.description": "Сообщение, которое отправляется получателям публикации. Доступные переменные:\n {creator} - Имя пользователя создателя загрузки\n {shareUrl} - URL загрузки\n {desc} - Описание загрузки\n {expires} - Дата истечения загрузки\n Переменные будут заменены на фактическое значение.",
"admin.config.email.reverse-share-subject": "Заголовок письма (обратная загрузка)",
"admin.config.email.reverse-share-subject.description": "Тема письма, которое отправляется, когда кто-то создал загрузку с вашей обратной ссылкой.",
"admin.config.email.reverse-share-message": "Сообщение письма обратной загрузки",
"admin.config.email.reverse-share-message.description": "Сообщение, которое отправляется, когда кто-то создал загрузку с вашей обратной ссылкой. {shareUrl} будет заменен именем создателя и URL-адресом общего доступа.",
"admin.config.email.reset-password-subject": "Тема сброса пароля",
"admin.config.email.reset-password-subject.description": "Тема письма, которое отправляется, когда пользователь запрашивает сброс пароля.",
"admin.config.email.reset-password-message": "Сообщение о сбросе пароля",
"admin.config.email.reset-password-message.description": "Сообщение, которое отправляется при запросе сброса пароля. {url} будет заменён ссылкой.",
"admin.config.email.invite-subject": "Тема приглашения",
"admin.config.email.invite-subject.description": "Тема письма, которое отправляется, когда администратор приглашает пользователя.",
"admin.config.email.invite-message": "Сообщение с приглашением",
"admin.config.email.invite-message.description": "Сообщение приглашения. {url} будет заменён ссылкой приглашения, а {password} паролем.",
"admin.config.share.allow-registration": "Разрешить регистрацию",
"admin.config.share.allow-registration.description": "Разрешена ли регистрация",
"admin.config.share.allow-unauthenticated-shares": "Разрешить неавторизованные загрузки",
"admin.config.share.allow-unauthenticated-shares.description": "Могут ли неавторизованные пользователи создавать загрузки",
"admin.config.share.max-size": "Максимальный размер",
"admin.config.share.max-size.description": "Максимальный размер файла в байтах",
"admin.config.share.zip-compression-level": "Уровень сжатия Zip",
"admin.config.share.zip-compression-level.description": "Регулировка уровня баланса между размером файла и скоростью сжатия. Допустимые значения от 0 до 9, с 0 без сжатия, а 9 - максимальное сжатие. ",
"admin.config.smtp.enabled": "Включено",
"admin.config.smtp.enabled.description": "Включено ли SMTP. Установите значение true только если вы ввели хост, порт, email, пользователь и пароль вашего SMTP-сервера.",
"admin.config.smtp.host": "Хост",
"admin.config.smtp.host.description": "Сервер SMTP сервера",
"admin.config.smtp.port": "Порт",
"admin.config.smtp.port.description": "Порт SMTP сервера",
"admin.config.smtp.email": "Электронная почта",
"admin.config.smtp.email.description": "Адрес электронной почты, от которого отправляются письма",
"admin.config.smtp.username": "Логин",
"admin.config.smtp.username.description": "Имя пользователя SMTP сервера",
"admin.config.smtp.password": "Пароль",
"admin.config.smtp.password.description": "Пароль SMTP-сервера",
"admin.config.smtp.button.test": "Отправить тестовое письмо",
// 404
"404.description": "Oops this page doesn't exist.",
"404.button.home": "Bring me back home",
"404.description": "Упс, этой страницы не существует.",
"404.button.home": "Верните меня домой",
// Common translations
"common.button.save": "Save",
"common.button.create": "Create",
"common.button.submit": "Submit",
"common.button.delete": "Delete",
"common.button.cancel": "Cancel",
"common.button.confirm": "Confirm",
"common.button.disable": "Disable",
"common.button.share": "Share",
"common.button.generate": "Generate",
"common.button.done": "Done",
"common.text.link": "Link",
"common.text.or": "or",
"common.button.go-back": "Go back",
"common.notify.copied": "Your link was copied to the clipboard",
"common.success": "Success",
"common.error": "Error",
"common.error.unknown": "An unknown error occurred",
"common.error.invalid-email": "Invalid email address",
"common.error.too-short": "Must be at least {length} characters",
"common.error.too-long": "Must be at most {length} characters",
"common.error.exact-length": "Must be exactly {length} characters",
"common.error.invalid-number": "Must be a number",
"common.error.field-required": "This field is required",
};
"common.button.save": "Сохранить",
"common.button.create": "Создать",
"common.button.submit": "Отправить",
"common.button.delete": "Удалить",
"common.button.cancel": "Отмена",
"common.button.confirm": "Подтвердить",
"common.button.disable": "Отключить",
"common.button.share": "Поделиться",
"common.button.generate": "Сгенерировать",
"common.button.done": "Готово",
"common.text.link": "Ссылка",
"common.text.or": "или",
"common.button.go-back": "Назад",
"common.notify.copied": "Ваша ссылка скопирована в буфер обмена",
"common.success": "Успешно",
"common.error": "Ошибочка",
"common.error.unknown": "Произошла неизвестная ошибка",
"common.error.invalid-email": "Недопустимый адрес электронной почты",
"common.error.too-short": "Должно быть не менее {length} символов",
"common.error.too-long": "Должно быть не больше {length} символов",
"common.error.exact-length": "Должно быть ровно {length} символов",
"common.error.invalid-number": "Должно быть числом",
"common.error.field-required": "Поле обязательно для заполнения"
};

View File

@@ -0,0 +1,324 @@
export default {
// Navbar
"navbar.upload": "อัพโหลด",
"navbar.signin": "เข้าสู่ระบบ",
"navbar.home": "หน้าหลัก",
"navbar.signup": "สมัครบัญชี",
"navbar.links.shares": "แชร์ของฉัน",
"navbar.links.reverse": "รีเวิร์สแชร์",
"navbar.avatar.account": "บัญชีของฉัน",
"navbar.avatar.admin": "แผงควบคุมระบบ",
"navbar.avatar.signout": "ออกจากระบบ",
// END navbar
// /
"home.title": "แพลตฟอร์มสำหรับแชร์ไฟล์ที่คุณสามารถโฮสต์ด้วยตนเอง.",
"home.description": "คุณอยากให้บริษัทภายนอกเช่น WeTransfer เข้าถึงไฟล์ส่วนตัวของคุณหรือเปล่า?",
"home.bullet.a.name": "Self-Hosted",
"home.bullet.a.description": "โฮสต์ Pingvin Share บนเครื่องของคุณเอง.",
"home.bullet.b.name": "ความเป็นส่วนตัว",
"home.bullet.b.description": "ไฟล์ของคุณคือไฟล์ของคุณ มันไม่ควรตกอยู่บนมือของบุคคลที่สาม",
"home.bullet.c.name": "ไม่มีการจำกัดขนาดไฟล์ที่น่ารำคาญ",
"home.bullet.c.description": "อัพโหลดใหญ่เท่าไหนก็ได้ จำกัดอย่างเดียวคือความจุของเคื่องคุณ",
"home.button.start": "เริ่มต้น",
"home.button.source": "ซอร์สโค้ด",
// END /
// /auth/signin
"signin.title": "ยินดีต้อนรับกลับ",
"signin.description": "ยังไม่มีบัญชี?",
"signin.button.signup": "สมัครบัญชี",
"signin.input.email-or-username": "อีเมล์์์์์์์์หรือชื่อผู้ใช้",
"signin.input.email-or-username.placeholder": "อีเมล์์์์์์์หรือชื่อผู้ใช้ของคุณ",
"signin.input.password": "รหัสผ่าน",
"signin.input.password.placeholder": "รหัสผ่านของคุณ",
"signin.button.submit": "เข้าสู่ระบบ",
"signIn.notify.totp-required.title": "ยืนยันตรวจสอบสิทธิ์สองปัจจัย",
"signIn.notify.totp-required.description": "กรุณาใส่รหัสยืนยันตัวตนสองปัจจัย",
// END /auth/signin
// /auth/signup
"signup.title": "สมัครบัญชี",
"signup.description": "มีบัญชีแล้ว?",
"signup.button.signin": "เข้าสู่ระบบ",
"signup.input.username": "ชื่อผู้ใช้",
"signup.input.username.placeholder": "ชื่อผู้ใช้ของคุณ",
"signup.input.email": "อีเมล์์์์์์์์",
"signup.input.email.placeholder": "อีเมล์์์์์์์์ของคุณ",
"signup.button.submit": "เริ่มต้นกัน",
// END /auth/signup
// /auth/reset-password
"resetPassword.title": "ลืมรหัสผ่าน?",
"resetPassword.description": "กรุณาใส่อีเมล์์์์์์์ของคุณเพื่อรีเซ็ตรหัสผ่าน",
"resetPassword.notify.success": "ส่งอีเมล์์์์์์์พร้อมลิงค์เพื่อรีเซ็ตรหัสผ่านของคุณแล้ว",
"resetPassword.button.back": "กลับไปที่หน้าลงชื่อเข้าใช้",
"resetPassword.text.resetPassword": "รีเซ็ตรหัสผ่าน",
"resetPassword.text.enterNewPassword": "ป้อนรหัสผ่านใหม่ของคุณ",
"resetPassword.input.password": "รหัสผ่านใหม่",
"resetPassword.notify.passwordReset": "รหัสผ่านของคุณรีเซ็ตเรียบร้อยแล้ว",
// /account
"account.title": "บัญชีของฉัน",
"account.card.info.title": "ข้อมูลบัญชี",
"account.card.info.username": "ชื่อผู้ใช้",
"account.card.info.email": "อีเมล์์์์์์์",
"account.notify.info.success": "อัปเดตบัญชีเรียบร้อยแล้ว",
"account.card.password.title": "รหัสผ่าน",
"account.card.password.old": "รหัสผ่านเก่า",
"account.card.password.new": "รหัสผ่านใหม่",
"account.notify.password.success": "อัปเดตรหัสผ่านเรียบร้อยแล้ว",
"account.card.security.title": "ความปลอดภัย",
"account.card.security.totp.enable.description": "ใส่รหัสผ่านปัจจุบันของคุณเพื่อเริ่มต้นการเปิดใช้งาน TOTP",
"account.card.security.totp.disable.description": "ใส่รหัสผ่านปัจจุบันของคุณเพื่อปิดใช้งาน TOTP",
"account.card.security.totp.button.start": "เริ่มต้น",
"account.modal.totp.title": "เปิดใช้งาน TOTP",
"account.modal.totp.step1": "ขั้นตอนที่ 1: สแกนรหัส QR",
"account.modal.totp.step2": "ขั้นตอนที่ 2: ป้อนรหัสยืนยันตัวตน",
"account.modal.totp.enterManually": "ป้อนด้วยตนเอง",
"account.modal.totp.code": "รหัส",
"account.modal.totp.clickToCopy": "คลิกเพื่อคัดลอก",
"account.modal.totp.verify": "ยืนยัน",
"account.notify.totp.disable": "TOTP ถูกปิดใช้งานเรียบร้อยแล้ว",
"account.notify.totp.enable": "TOTP ถูกเปิดใช้งานเรียบร้อยแล้ว",
"account.card.language.title": "ภาษา",
"account.card.language.description": "โปรเจคนี้ถูกแปลโดยชุมชน บางภาษาอาจยังไม่สมบูรณ์",
"account.card.color.title": "ธีมสี",
// ThemeSwitcher.tsx
"account.theme.dark": "มืด",
"account.theme.light": "สว่าง",
"account.theme.system": "ตามระบบ",
"account.button.delete": "ลบบัญชี",
"account.modal.delete.title": "ลบบัญชีของคุณ",
"account.modal.delete.description": "คุณต้องการลบบัญชีของคุณหรือไม่ รวมถึงแชร์ที่คุณสร้างไว้ทั้งหมด?",
// END /account
// /account/shares
"account.shares.title": "แชร์ของฉัน",
"account.shares.title.empty": "มันว่างเปล่าที่นี่ 👀",
"account.shares.description.empty": "คุณยังไม่ได้สร้างแชร์ใดๆ",
"account.shares.button.create": "สร้างแชร์",
"account.shares.info.title": "ข้อมูลแชร์",
"account.shares.table.id": "ID",
"account.shares.table.name": "ชื่อ",
"account.shares.table.description": "คำอธิบาย",
"account.shares.table.visitors": "ผู้เข้าชม",
"account.shares.table.expiresAt": "หมดอายุ",
"account.shares.table.createdAt": "สร้างเมื่อ",
"account.shares.table.size": "ขนาด",
"account.shares.modal.share-informations": "ข้อมูลแชร์",
"account.shares.modal.share-link": "แชร์ลิงค์",
"account.shares.modal.delete.title": "ลบแชร์ {share}",
"account.shares.modal.delete.description": "คุณต้องการลบแชร์นี้หรือไม่?",
// END /account/shares
// /account/reverseShares
"account.reverseShares.title": "รีเวิร์สแชร์ของฉัน",
"account.reverseShares.description": "รีเวิร์สแชร์สร้างลิงค์สำหรับคนภายนอกเพื่อที่จะแชร์ไฟล์ให้คุณ",
"account.reverseShares.title.empty": "มันว่างเปล่าที่นี่ 👀",
"account.reverseShares.description.empty": "คุณยังไม่ได้สร้างรีเวิร์สแชร์ใดๆ",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "ลิงค์หมดอายุใน",
"account.reverseShares.modal.expiration.minute-singular": "นาที",
"account.reverseShares.modal.expiration.minute-plural": "นาที",
"account.reverseShares.modal.expiration.hour-singular": "ชั่วโมง",
"account.reverseShares.modal.expiration.hour-plural": "ชั่วโมง",
"account.reverseShares.modal.expiration.day-singular": "วัน",
"account.reverseShares.modal.expiration.day-plural": "วัน",
"account.reverseShares.modal.expiration.week-singular": "สัปดาห์",
"account.reverseShares.modal.expiration.week-plural": "สัปดาห์",
"account.reverseShares.modal.expiration.month-singular": "เดือน",
"account.reverseShares.modal.expiration.month-plural": "เดือน",
"account.reverseShares.modal.expiration.year-singular": "ปี",
"account.reverseShares.modal.expiration.year-plural": "ปี",
"account.reverseShares.modal.max-size.label": "ขนาดสูงสุดของแชร์",
"account.reverseShares.modal.send-email": "ส่งอีเมล์์์์์์์แจ้งเตือน",
"account.reverseShares.modal.send-email.description": "ส่งอีเมล์์์์์์์แจ้งเตือนเมื่อมีการสร้างแชร์ด้วยลิงค์รีเวิร์สนี้",
"account.reverseShares.modal.max-use.label": "จำนวนการใช้งานสูงสุด",
"account.reverseShares.modal.max-use.description": "จำนวนครั้งสูงสุดที่ลิงค์นี้สามารถใช้งานได้",
"account.reverseShare.never-expires": "ลิงค์นี้ไม่มีวันหมดอายุ",
"account.reverseShare.expires-on": "ลิงค์นี้จะหมดอายุใน {expiration}.",
"account.reverseShares.table.no-shares": "ยังไม่มีการสร้างแชร์",
"account.reverseShares.table.count.singular": "แชร์",
"account.reverseShares.table.count.plural": "แชร์",
"account.reverseShares.table.shares": "แชร์",
"account.reverseShares.table.remaining": "เหลืออีก {count} ครั้ง",
"account.reverseShares.table.max-size": "ขนาดสูงสุดของแชร์",
"account.reverseShares.table.expires": "หมดอายุ",
"account.reverseShares.modal.reverse-share-link": "ลิงค์รีเวิร์สแชร์",
"account.reverseShares.modal.delete.title": "ลบลิงค์รีเวิร์สแชร์",
"account.reverseShares.modal.delete.description": "คุณต้องการลบลิงค์รีเวิร์สแชร์นี้หรือไม่? หากคุณทำเช่นนั้นแชร์ที่เกี่ยวข้องจะถูกลบด้วย",
// END /account/reverseShares
// /admin
"admin.title": "แผงควบคุมระบบ",
"admin.button.users": "การจัดการผู้ใช้",
"admin.button.config": "การตั้งค่า",
"admin.version": "เวอร์ชัน",
// END /admin
// /admin/users
"admin.users.title": "การจัดการผู้ใช้",
"admin.users.table.username": "ชื่อผู้ใช้",
"admin.users.table.email": "อีเมล์์์์์์์",
"admin.users.table.admin": "ผู้ดูแลระบบ",
"admin.users.edit.update.title": "อัปเดตผู้ใช้ {username}",
"admin.users.edit.update.admin-privileges": "สิทธิ์ของผู้ดูแลระบบ",
"admin.users.edit.update.change-password.title": "เปลี่ยนรหัสผ่าน",
"admin.users.edit.update.change-password.field": "รหัสผ่านใหม่",
"admin.users.edit.update.change-password.button": "บันทึกรหัสผ่านใหม่",
"admin.users.edit.update.notify.password.success": "รหัสผ่านเปลี่ยนเรียบร้อยแล้ว",
"admin.users.edit.delete.title": "ลบผู้ใช้ {username}",
"admin.users.edit.delete.description": "คุณต้องการลบผู้ใช้นี้และการแชร์ทั้งหมดของเขาหรือไม่?",
// showCreateUserModal.tsx
"admin.users.modal.create.title": "สร้างผู้ใช้",
"admin.users.modal.create.username": "ชื่อผู้ใช้",
"admin.users.modal.create.email": "อีเมล์",
"admin.users.modal.create.password": "รหัสผ่าน",
"admin.users.modal.create.manual-password": "ตั้งรหัสผ่านด้วยตนเอง",
"admin.users.modal.create.manual-password.description": "หากไม่ติ๊กเลือก ผู้ใช้จะได้รับอีเมล์พร้อมลิงก์เพื่อตั้งรหัสผ่านด้วยตนเอง",
"admin.users.modal.create.admin": "สิทธิ์ของผู้ดูแลระบบ",
"admin.users.modal.create.admin.description": "หากติ๊กเลือก ผู้ใช้จะสามารถเข้าถึงแผงควบคุมระบบได้",
// END /admin/users
// /upload
"upload.title": "อัปโหลด",
"upload.notify.generic-error": "เกิดข้อผิดพลาดขณะที่กำลังจัดการการแชร์ของคุณ",
"upload.notify.count-failed": "มีไฟล์จำนวน {count} ไฟล์ที่ไม่สามารถอัปโหลดได้ กำลังลองอีกครั้ง",
// Dropzone.tsx
"upload.dropzone.title": "อัปโหลดไฟล์",
"upload.dropzone.description": "ลากและวางไฟล์ที่นี่เพื่อเริ่มการแชร์ของคุณ สามารถรับไฟล์ที่มีขนาดรวมไม่เกิน {maxSize} เท่านั้น",
"upload.dropzone.notify.file-too-big": "ไฟล์ของคุณเกินขนาดสูงสุดของการแชร์ {maxSize}",
// FileList.tsx
"upload.filelist.name": "ชื่อ",
"upload.filelist.size": "ขนาด",
// showCreateUploadModal.tsx
"upload.modal.title": "สร้างการแชร์",
"upload.modal.link.error.invalid": "สามารถใช้ได้เฉพาะตัวอักษร ตัวเลข ขีดล่าง และขีดเส้น",
"upload.modal.link.error.taken": "ลิงก์นี้ถูกใช้งานแล้ว",
"upload.modal.not-signed-in": "คุณยังไม่ได้เข้าสู่ระบบ",
"upload.modal.not-signed-in-description": "คุณจะไม่สามารถลบการแชร์ของคุณด้วยตนเองและดูจำนวนผู้เข้าชมได้",
"upload.modal.expires.never": "ไม่มีกำหนด",
"upload.modal.expires.never-long": "ไม่มีกำหนดหมดอายุ",
"upload.modal.link.label": "ลิงค์",
"upload.modal.expires.label": "หมดอายุ",
"upload.modal.expires.minute-singular": "นาที",
"upload.modal.expires.minute-plural": "นาที",
"upload.modal.expires.hour-singular": "ชั่วโมง",
"upload.modal.expires.hour-plural": "ชั่วโมง",
"upload.modal.expires.day-singular": "วัน",
"upload.modal.expires.day-plural": "วัน",
"upload.modal.expires.week-singular": "สัปดาห์",
"upload.modal.expires.week-plural": "สัปดาห์",
"upload.modal.expires.month-singular": "เดือน",
"upload.modal.expires.month-plural": "เดือน",
"upload.modal.expires.year-singular": "ปี",
"upload.modal.expires.year-plural": "ปี",
"upload.modal.accordion.description.title": "คำอธิบาย",
"upload.modal.accordion.description.placeholder": "หมายเหตุสำหรับผู้รับการแชร์นี้",
"upload.modal.accordion.email.title": "ผู้รับอีเมล์",
"upload.modal.accordion.email.placeholder": "ป้อนผู้รับอีเมล์",
"upload.modal.accordion.email.invalid-email": "ที่อยู่อีเมล์ไม่ถูกต้อง",
"upload.modal.accordion.security.title": "ตัวเลือกความปลอดภัย",
"upload.modal.accordion.security.password.label": "การป้องกันรหัสผ่าน",
"upload.modal.accordion.security.password.placeholder": "ไม่มีรหัสผ่าน",
"upload.modal.accordion.security.max-views.label": "จำนวนการเข้าชมสูงสุด",
"upload.modal.accordion.security.max-views.placeholder": "ไม่จำกัด",
// showCompletedUploadModal.tsx
"upload.modal.completed.never-expires": "การแชร์นี้จะไม่มีวันหมดอายุ",
"upload.modal.completed.expires-on": "การแชร์นี้จะหมดอายุเมื่อวันที่ {expiration}",
"upload.modal.completed.share-ready": "แชร์พร้อมใช้งาน",
// END /upload
// /share/[id]
"share.title": "แชร์ {shareId}",
"share.description": "ดูสิ่งที่ฉันแชร์กับคุณ!",
"share.error.visitor-limit-exceeded.title": "เกินขีดจำกัดผู้เข้าชม",
"share.error.visitor-limit-exceeded.description": "การแชร์นี้ได้เกินขีดจำกัดผู้เข้าชมแล้ว",
"share.error.removed.title": "การแชร์ถูกลบ",
"share.error.not-found.title": "ไม่พบการแชร์นี้",
"share.error.not-found.description": "การแชร์ที่คุณกำลังมองหาไม่มีอยู่จริง",
"share.modal.password.title": "ต้องการรหัสผ่าน",
"share.modal.password.description": "กรุณาป้อนรหัสผ่านเพื่อเข้าถึงการแชร์นี้",
"share.modal.password": "รหัสผ่าน",
"share.modal.error.invalid-password": "รหัสผ่านไม่ถูกต้อง",
"share.button.download-all": "ดาวน์โหลดทั้งหมด",
"share.notify.download-all-preparing": "กำลังเตรียมการแชร์นี้ โปรดลองอีกครั้งในไม่กี่นาที",
"share.modal.file-link": "ลิงค์ไฟล์",
"share.table.name": "ชื่อ",
"share.table.size": "ขนาด",
"share.modal.file-preview.error.not-supported.title": "ไม่รองรับการแสดงตัวอย่าง",
"share.modal.file-preview.error.not-supported.description": "ไม่รองรับการแสดงตัวอย่างสำหรับไฟล์ประเภทนี้ โปรดดาวน์โหลดไฟล์เพื่อดู",
// END /share/[id]
// /admin/config
"admin.config.title": "การตั้งค่า",
"admin.config.category.general": "ทั่วไป",
"admin.config.category.share": "การแชร์",
"admin.config.category.email": "อีเมล์์์์์์",
"admin.config.category.smtp": "SMTP",
"admin.config.general.app-name": "ชื่อแอพ",
"admin.config.general.app-name.description": "ชื่อแอพพลิเคชัน",
"admin.config.general.app-url": "URL ของแอพ",
"admin.config.general.app-url.description": "URL ที่สามารถเข้าถึงแอพพลิเคชัน Pingvin Share ได้",
"admin.config.general.show-home-page": "แสดงหน้าแรก",
"admin.config.general.show-home-page.description": "หากติ๊ก เว็บไซต์จะแสดงหน้าหลักเวลาเข้าถึง URL หลัก",
"admin.config.general.logo": "โลโก้",
"admin.config.general.logo.description": "เปลี่ยนโลโก้โดยอัปโหลดรูปภาพใหม่ รูปภาพต้องเป็น PNG และควรมีขนาดอัตราส่วน 1:1",
"admin.config.general.logo.placeholder": "คลิกที่นี่หรือลากไฟล์มา",
"admin.config.email.enable-share-email-recipients": "เปิดใช้งานผู้รับอีเมล์์์์์์ของการแชร์",
"admin.config.email.enable-share-email-recipients.description": "เมื่อเปิดใช้งานผู้รับอีเมล์์์์์์ของการแชร์ ผู้ใช้ที่สร้างการแชร์จะสามารถเพิ่มผู้รับอีเมล์์์์์์์ของการแชร์ได้ เปิดเฉพาะเมื่อคุณเปิดใช้งาน SMTP",
"admin.config.email.share-recipients-subject": "หัวเรื่องผู้รับอีเมล์์์์์์ของการแชร์",
"admin.config.email.share-recipients-subject.description": "หัวเรื่องของอีเมล์์์์์์์์ที่ส่งไปยังผู้รับอีเมล์์์์์์ของการแชร์",
"admin.config.email.share-recipients-message": "ข้อความผู้รับอีเมล์์์์์์ของการแชร์",
"admin.config.email.share-recipients-message.description": "ข้อความที่ส่งไปยังผู้รับอีเมล์์์์์์ของการแชร์ ตัวแปรที่ใช้ได้:\n {creator} - ชื่อผู้สร้างการแชร์\n {shareUrl} - URL ของการแชร์\n {desc} - คำอธิบายของการแชร์\n {expires} - วันหมดอายุของการแชร์\n ตัวแปรจะถูกแทนที่ด้วยค่าจริง",
"admin.config.email.reverse-share-subject": "หัวเรื่องการแชร์รีเวิร์ส",
"admin.config.email.reverse-share-subject.description": "หัวเรื่องของอีเมล์์์์์์ที่ส่งไปยังผู้สร้างการแชร์รีเวิร์ส",
"admin.config.email.reverse-share-message": "ข้อความการแชร์รีเวิร์ส",
"admin.config.email.reverse-share-message.description": "ข้อความที่ส่งไปยังผู้สร้างการแชร์รีเวิร์ส ตัวแปรที่ใช้ได้:\n {shareUrl} - ชื่อผู้สร้างแชร์รีเวิร์สและ URL ของการแชร์\n ตัวแปรจะถูกแทนที่ด้วยค่าจริง",
"admin.config.email.reset-password-subject": "หัวเรื่องการรีเซ็ตรหัสผ่าน",
"admin.config.email.reset-password-subject.description": "หัวเรื่องของอีเมล์์์์์์ที่ส่งไปยังผู้ใช้เมื่อขอรีเซ็ตรหัสผ่าน",
"admin.config.email.reset-password-message": "ข้อความการรีเซ็ตรหัสผ่าน",
"admin.config.email.reset-password-message.description": "ข้อความที่ส่งไปยังผู้ใช้เมื่อขอรีเซ็ตรหัสผ่าน ตัวแปรที่ใช้ได้:\n {url} - URL สำหรับรีเซ็ตรหัสผ่าน\n ตัวแปรจะถูกแทนที่ด้วยค่าจริง",
"admin.config.email.invite-subject": "หัวเรื่องการเชิญ",
"admin.config.email.invite-subject.description": "หัวเรื่องของอีเมล์์์์์์ที่ส่งไปยังผู้ใช้เมื่อผู้ดูแลระบบเชิญผู้ใช้",
"admin.config.email.invite-message": "ข้อความการเชิญ",
"admin.config.email.invite-message.description": "ข้อความที่ส่งไปยังผู้ใช้เมื่อผู้ดูแลระบบเชิญผู้ใช้ ตัวแปรที่ใช้ได้:\n {url} - URL สำหรับเชิญผู้ใช้\n {password} - รหัสผ่านของผู้ใช้\n ตัวแปรจะถูกแทนที่ด้วยค่าจริง",
"admin.config.share.allow-registration": "อนุญาตให้ลงทะเบียนด้วยตัวเอง",
"admin.config.share.allow-registration.description": "อนุญาตให้ผู้ใช้ลงทะเบียนด้วยตัวเองเพื่อสร้างแชร์",
"admin.config.share.allow-unauthenticated-shares": "อนุญาตให้แชร์โดยไม่ต้องเข้าสู่ระบบ",
"admin.config.share.allow-unauthenticated-shares.description": "อนุญาตให้ผู้ใช้ที่ไม่ได้เข้าสู่ระบบสร้างแชร์",
"admin.config.share.max-size": "ขนาดสูงสุด",
"admin.config.share.max-size.description": "ขนาดสูงสุดของแชร์",
"admin.config.share.zip-compression-level": "ระดับการบีบอัดไฟล์ Zip",
"admin.config.share.zip-compression-level.description": "ปรับระดับเพื่อปรับความสมดุลระหว่างขนาดไฟล์และความเร็วในการบีบอัด ค่าอยู่ระหว่าง 0-9 โดย 0 คือไม่มีการบีบอัดและ 9 คือการบีบอัดสูงสุด",
"admin.config.smtp.enabled": "เปิด",
"admin.config.smtp.enabled.description": "เปิดใช้งาน SMTP สำหรับการส่งอีเมล์์์์์์ เปิดได้เท่านั้นต่อเมื่อคุณใส่ข้อมูลโฮสต์ พอร์ต อีเมล์ ผู้ใช้ และรหัสผ่านของเซิร์ฟเวอร์ SMTP ของคุณ",
"admin.config.smtp.host": "โฮสต์",
"admin.config.smtp.host.description": "โฮสต์ของเซิร์ฟเวอร์ SMTP",
"admin.config.smtp.port": "พอร์ต",
"admin.config.smtp.port.description": "พอร์ตของเซิร์ฟเวอร์ SMTP",
"admin.config.smtp.email": "อีเมล์์์์์์์",
"admin.config.smtp.email.description": "อีเมล์์์์์์์ที่ใช้สำหรับการส่งอีเมล์์์์์์์",
"admin.config.smtp.username": "ชื่อผู้ใช้",
"admin.config.smtp.username.description": "ชื่อผู้ใช้ของเซิร์ฟเวอร์ SMTP",
"admin.config.smtp.password": "รหัสผ่าน",
"admin.config.smtp.password.description": "รหัสผ่านของเซิร์ฟเวอร์ SMTP",
"admin.config.smtp.button.test": "ส่งอีเมล์์์์์์ทดสอบ",
// 404
"404.description": "ไม่พบหน้าที่คุณกำลังมองหา",
"404.button.home": "หน้าแรก",
// Common translations
"common.button.save": "บันทึก",
"common.button.create": "สร้าง",
"common.button.submit": "ส่ง",
"common.button.delete": "ลบ",
"common.button.cancel": "ยกเลิก",
"common.button.confirm": "ยืนยัน",
"common.button.disable": "ปิดการใช้งาน",
"common.button.share": "แชร์",
"common.button.generate": "สุ่ม",
"common.button.done": "เสร็จสิ้น",
"common.text.link": "ลิงค์",
"common.text.or": "หรือ",
"common.button.go-back": "ย้อนกลับ",
"common.notify.copied": "คัดลอกไปยังคลิปบอร์ดแล้ว",
"common.success": "สำเร็จ",
"common.error": "เกิดข้อผิดพลาด",
"common.error.unknown": "เกิดข้อผิดพลาดที่ไม่รู้จัก",
"common.error.invalid-email": "ที่อยู่อีเมล์ไม่ถูกต้อง",
"common.error.too-short": "ต้องมีอย่างน้อย {length} ตัวอักษร",
"common.error.too-long": "ต้องมีไม่เกิน {length} ตัวอักษร",
"common.error.exact-length": "ต้องมีความยาว {length} ตัวอักษร",
"common.error.invalid-number": "ต้องเป็นตัวเลข",
"common.error.field-required": "ต้องกรอกข้อมูลนี้"
};

View File

@@ -111,6 +111,7 @@ export default {
"account.reverseShares.title.empty": "这里空空如也 👀",
"account.reverseShares.description.empty": "你没有创建任何预留共享",
// showCreateReverseShareModal.tsx
"account.reverseShares.modal.title": "Create reverse share",
"account.reverseShares.modal.expiration.label": "过期时间",
"account.reverseShares.modal.expiration.minute-singular": "1 分钟",
"account.reverseShares.modal.expiration.minute-plural": "分钟",

View File

@@ -35,7 +35,7 @@ export async function middleware(request: NextRequest) {
try {
const claims = jwtDecode<{ exp: number; isAdmin: boolean }>(
accessToken as string
accessToken as string,
);
if (claims.exp * 1000 > Date.now()) {
user = claims;

View File

@@ -43,9 +43,7 @@ const ErrorNotFound = () => {
<>
<Meta title="Not found" />
<Container className={classes.root}>
<div className={classes.label}>
404
</div>
<div className={classes.label}>404</div>
<Title align="center" order={3}>
<FormattedMessage id="404.description" />
</Title>

View File

@@ -41,7 +41,7 @@ function App({ Component, pageProps }: AppProps) {
const [route, setRoute] = useState<string>(pageProps.route);
const [configVariables, setConfigVariables] = useState<Config[]>(
pageProps.configVariables
pageProps.configVariables,
);
useEffect(() => {
@@ -165,7 +165,7 @@ App.getInitialProps = async ({ ctx }: { ctx: GetServerSidePropsContext }) => {
pageProps.route = ctx.req.url;
const requestLanguage = i18nUtil.getLanguageFromAcceptHeader(
ctx.req.headers["accept-language"]
ctx.req.headers["accept-language"],
);
pageProps.language = ctx.req.cookies["language"] ?? requestLanguage;

View File

@@ -17,6 +17,7 @@ import { Tb2Fa } from "react-icons/tb";
import { FormattedMessage } from "react-intl";
import * as yup from "yup";
import Meta from "../../components/Meta";
import LanguagePicker from "../../components/account/LanguagePicker";
import ThemeSwitcher from "../../components/account/ThemeSwitcher";
import showEnableTotpModal from "../../components/account/showEnableTotpModal";
import useTranslate from "../../hooks/useTranslate.hook";
@@ -24,7 +25,6 @@ import useUser from "../../hooks/user.hook";
import authService from "../../services/auth.service";
import userService from "../../services/user.service";
import toast from "../../utils/toast.util";
import LanguagePicker from "../../components/account/LanguagePicker";
const Account = () => {
const { user, refreshUser } = useUser();
@@ -42,7 +42,7 @@ const Account = () => {
username: yup
.string()
.min(3, t("common.error.too-short", { length: 3 })),
})
}),
),
});
@@ -61,7 +61,7 @@ const Account = () => {
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
}),
),
});
@@ -75,7 +75,7 @@ const Account = () => {
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
}),
),
});
@@ -92,7 +92,7 @@ const Account = () => {
.min(6, t("common.error.exact-length", { length: 6 }))
.max(6, t("common.error.exact-length", { length: 6 }))
.matches(/^[0-9]+$/, { message: t("common.error.invalid-number") }),
})
}),
),
});
@@ -115,7 +115,7 @@ const Account = () => {
email: values.email,
})
.then(() => toast.success(t("account.notify.info.success")))
.catch(toast.axiosError)
.catch(toast.axiosError),
)}
>
<Stack>
@@ -147,7 +147,7 @@ const Account = () => {
toast.success(t("account.notify.password.success"));
passwordForm.reset();
})
.catch(toast.axiosError)
.catch(toast.axiosError),
)}
>
<Stack>
@@ -181,7 +181,7 @@ const Account = () => {
</Tabs.List>
<Tabs.Panel value="totp" pt="xs">
{user!.totpVerified ? (
{user?.totpVerified ? (
<>
<form
onSubmit={disableTotpForm.onSubmit((values) => {
@@ -199,7 +199,7 @@ const Account = () => {
<Stack>
<PasswordInput
description={t(
"account.card.security.totp.disable.description"
"account.card.security.totp.disable.description",
)}
label={t("account.card.password.title")}
{...disableTotpForm.getInputProps("password")}
@@ -241,7 +241,7 @@ const Account = () => {
<PasswordInput
label={t("account.card.password.title")}
description={t(
"account.card.security.totp.enable.description"
"account.card.security.totp.enable.description",
)}
{...enableTotpForm.getInputProps("password")}
/>

View File

@@ -77,7 +77,7 @@ const MyShares = () => {
showCreateReverseShareModal(
modals,
config.get("smtp.enabled"),
getReverseShares
getReverseShares,
)
}
leftIcon={<TbPlus size={20} />}
@@ -134,10 +134,10 @@ const MyShares = () => {
<Text size="sm">
{reverseShare.shares.length == 1
? `1 ${t(
"account.reverseShares.table.count.singular"
"account.reverseShares.table.count.singular",
)}`
: `${reverseShare.shares.length} ${t(
"account.reverseShares.table.count.plural"
"account.reverseShares.table.count.plural",
)}`}
</Text>
</Accordion.Control>
@@ -158,15 +158,13 @@ const MyShares = () => {
size={25}
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${appUrl}/s/${share.id}`
);
clipboard.copy(`${appUrl}/s/${share.id}`);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
share.id,
config.get("general.appUrl")
config.get("general.appUrl"),
);
}
}}
@@ -200,14 +198,14 @@ const MyShares = () => {
clipboard.copy(
`${config.get("general.appUrl")}/upload/${
reverseShare.token
}`
}`,
);
toast.success(t("common.notify.copied"));
} else {
showReverseShareLinkModal(
modals,
reverseShare.token,
config.get("general.appUrl")
config.get("general.appUrl"),
);
}
}}
@@ -221,7 +219,7 @@ const MyShares = () => {
onClick={() => {
modals.openConfirmModal({
title: t(
"account.reverseShares.modal.delete.title"
"account.reverseShares.modal.delete.title",
),
children: (
<Text size="sm">
@@ -239,8 +237,8 @@ const MyShares = () => {
shareService.removeReverseShare(reverseShare.id);
setReverseShares(
reverseShares.filter(
(item) => item.id !== reverseShare.id
)
(item) => item.id !== reverseShare.id,
),
);
},
});

View File

@@ -119,7 +119,7 @@ const MyShares = () => {
modals,
share,
config.get("general.appUrl"),
parseInt(config.get("share.maxSize"))
parseInt(config.get("share.maxSize")),
);
}}
>
@@ -132,16 +132,14 @@ const MyShares = () => {
onClick={() => {
if (window.isSecureContext) {
clipboard.copy(
`${config.get("general.appUrl")}/s/${
share.id
}`
`${config.get("general.appUrl")}/s/${share.id}`,
);
toast.success(t("common.notify.copied"));
} else {
showShareLinkModal(
modals,
share.id,
config.get("general.appUrl")
config.get("general.appUrl"),
);
}
}}
@@ -172,7 +170,7 @@ const MyShares = () => {
onConfirm: () => {
shareService.remove(share.id);
setShares(
shares.filter((item) => item.id !== share.id)
shares.filter((item) => item.id !== share.id),
);
},
});

View File

@@ -56,7 +56,7 @@ export default function AppShellDemo() {
.then(() => {
setLogo(null);
toast.success(
"Logo updated successfully. It may take a few minutes to update on the website."
"Logo updated successfully. It may take a few minutes to update on the website.",
);
})
.catch(toast.axiosError);
@@ -76,7 +76,7 @@ export default function AppShellDemo() {
const updateConfigVariable = (configVariable: UpdateConfig) => {
const index = updatedConfigVariables.findIndex(
(item) => item.key === configVariable.key
(item) => item.key === configVariable.key,
);
if (index > -1) {
@@ -139,7 +139,7 @@ export default function AppShellDemo() {
<Title order={6}>
<FormattedMessage
id={`admin.config.${camelToKebab(
configVariable.key
configVariable.key,
)}`}
/>
</Title>
@@ -154,7 +154,7 @@ export default function AppShellDemo() {
>
<FormattedMessage
id={`admin.config.${camelToKebab(
configVariable.key
configVariable.key,
)}.description`}
values={{ br: <br /> }}
/>

View File

@@ -39,7 +39,7 @@ const ResetPassword = () => {
.string()
.min(8, t("common.error.too-short", { length: 8 }))
.required(t("common.error.field-required")),
})
}),
),
});

View File

@@ -57,7 +57,7 @@ const ResetPassword = () => {
.string()
.email(t("common.error.invalid-email"))
.required(t("common.error.field-required")),
})
}),
),
});
@@ -79,7 +79,7 @@ const ResetPassword = () => {
toast.success(t("resetPassword.notify.success"));
router.push("/auth/signIn");
})
.catch(toast.axiosError)
.catch(toast.axiosError),
)}
>
<TextInput

View File

@@ -36,7 +36,7 @@ const Share = ({ shareId }: { shareId: string }) => {
showErrorModal(
modals,
t("share.error.visitor-limit-exceeded.title"),
t("share.error.visitor-limit-exceeded.description")
t("share.error.visitor-limit-exceeded.description"),
);
} else {
toast.axiosError(e);
@@ -57,13 +57,13 @@ const Share = ({ shareId }: { shareId: string }) => {
showErrorModal(
modals,
t("share.error.removed.title"),
e.response.data.message
e.response.data.message,
);
} else {
showErrorModal(
modals,
t("share.error.not-found.title"),
t("share.error.not-found.description")
t("share.error.not-found.description"),
);
}
} else if (error == "share_password_required") {

View File

@@ -29,7 +29,7 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
showErrorModal(
modals,
"Invalid Link",
"This link is invalid. Please check your link."
"This link is invalid. Please check your link.",
);
setIsLoading(false);
});

View File

@@ -56,7 +56,7 @@ const Upload = ({
file.uploadingProgress = progress;
}
return file;
})
}),
);
};
@@ -84,7 +84,7 @@ const Upload = ({
name: file.name,
},
chunkIndex,
chunks
chunks,
)
.then((response) => {
fileId = response.id;
@@ -114,7 +114,7 @@ const Upload = ({
}
}
}
})
}),
);
Promise.all(fileUploadPromises);
@@ -123,7 +123,7 @@ const Upload = ({
useEffect(() => {
// Check if there are any files that failed to upload
const fileErrorCount = files.filter(
(file) => file.uploadingProgress == -1
(file) => file.uploadingProgress == -1,
).length;
if (fileErrorCount > 0) {
@@ -133,7 +133,7 @@ const Upload = ({
{
withCloseButton: false,
autoClose: false,
}
},
);
}
errorToastShown = true;
@@ -174,13 +174,13 @@ const Upload = ({
isReverseShare,
appUrl: config.get("general.appUrl"),
allowUnauthenticatedShares: config.get(
"share.allowUnauthenticatedShares"
"share.allowUnauthenticatedShares",
),
enableEmailRecepients: config.get(
"email.enableShareEmailRecipients"
"email.enableShareEmailRecipients",
),
},
uploadFiles
uploadFiles,
);
}}
>

View File

@@ -19,7 +19,7 @@ const signInTotp = async (
emailOrUsername: string,
password: string,
totp: string,
loginToken: string
loginToken: string,
) => {
const emailOrUsernameBody = emailOrUsername.includes("@")
? { email: emailOrUsername }

View File

@@ -18,7 +18,7 @@ const get = (key: string, configVariables: Config[]): any => {
if (!configVariables) return null;
const configVariable = configVariables.filter(
(variable) => variable.key == key
(variable) => variable.key == key,
)[0];
if (!configVariable) throw new Error(`Config variable ${key} not found`);
@@ -42,7 +42,7 @@ const sendTestEmail = async (email: string) => {
const isNewReleaseAvailable = async () => {
const response = (
await axios.get(
"https://api.github.com/repos/stonith404/pingvin-share/releases/latest"
"https://api.github.com/repos/stonith404/pingvin-share/releases/latest",
)
).data;
return response.tag_name.replace("v", "") != process.env.VERSION;

View File

@@ -71,7 +71,7 @@ const uploadFile = async (
name: string;
},
chunkIndex: number,
totalChunks: number
totalChunks: number,
): Promise<FileUploadResponse> => {
const data = readerEvent.target!.result;
@@ -92,7 +92,7 @@ const createReverseShare = async (
shareExpiration: string,
maxShareSize: number,
maxUseCount: number,
sendEmailNotification: boolean
sendEmailNotification: boolean,
) => {
return (
await api.post("reverseShares", {

View File

@@ -26,7 +26,7 @@ export type AdminConfigGroupedByCategory = {
description: string;
obscured: boolean;
category: string;
}
},
];
};

View File

@@ -11,7 +11,7 @@ export const getExpirationPreview = (
expiration_num: number;
expiration_unit: string;
};
}
},
) => {
const value = form.values.never_expires
? "never"
@@ -21,12 +21,12 @@ export const getExpirationPreview = (
const expirationDate = moment()
.add(
value.split("-")[0],
value.split("-")[1] as moment.unitOfTime.DurationConstructor
value.split("-")[1] as moment.unitOfTime.DurationConstructor,
)
.toDate();
return messages.expiresOn.replace(
"{expiration}",
moment(expirationDate).format("LLL")
moment(expirationDate).format("LLL"),
);
};

View File

@@ -12,7 +12,7 @@ const getLanguageFromAcceptHeader = (acceptLanguage?: string) => {
const languages = acceptLanguage.split(",").map((l) => l.split(";")[0]);
const supportedLanguages = Object.values(LOCALES).map((l) => l.code);
const supportedLanguagesWithoutRegion = supportedLanguages.map(
(l) => l.split("-")[0]
(l) => l.split("-")[0],
);
for (const language of languages) {
@@ -23,7 +23,7 @@ const getLanguageFromAcceptHeader = (acceptLanguage?: string) => {
supportedLanguagesWithoutRegion.includes(language.split("-")[0])
) {
const similarLanguage = supportedLanguages.find((l) =>
l.startsWith(language.split("-")[0])
l.startsWith(language.split("-")[0]),
);
return similarLanguage;
}

View File

@@ -20,7 +20,7 @@ const axiosError = (axiosError: any) =>
const success = (
message: string,
config?: Omit<NotificationProps, "message">
config?: Omit<NotificationProps, "message">,
) =>
showNotification({
icon: <TbCheck />,

View File

@@ -1,6 +1,6 @@
{
"name": "pingvin-share",
"version": "0.17.2",
"version": "0.17.5",
"scripts": {
"format": "cd frontend && npm run format && cd ../backend && npm run format",
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",