1
0
mirror of https://github.com/stonith404/pingvin-share.git synced 2024-11-04 23:10:13 +01:00

feat(localization): Added thai language (#231)

* feat(localization): Added Thai translation

* Formatted

---------

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
iUnstable0 2023-08-17 19:54:26 +07:00 committed by GitHub
parent 18c10c0ac6
commit bddb87b9b3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 553 additions and 105 deletions

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`,
);
}
}

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

@ -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")}
/>

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

@ -0,0 +1,438 @@
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.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]
// 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 ตัวแปรจะถูกแทนที่ด้วยค่าจริง",
// "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": "หัวเรื่องการเชิญ",
"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": "ต้องกรอกข้อมูลนี้",
};