diff --git a/apps/nestjs-backend/package.json b/apps/nestjs-backend/package.json index 8082b190c8..6de2675570 100644 --- a/apps/nestjs-backend/package.json +++ b/apps/nestjs-backend/package.json @@ -204,6 +204,7 @@ "nanoid": "3.3.7", "nest-knexjs": "0.0.22", "nestjs-cls": "4.3.0", + "nestjs-i18n": "10.5.1", "nestjs-pino": "4.4.1", "nestjs-redoc": "2.2.2", "next": "14.2.14", diff --git a/apps/nestjs-backend/src/features/auth/local-auth/local-auth.service.ts b/apps/nestjs-backend/src/features/auth/local-auth/local-auth.service.ts index 467a4d6e8f..98131657f4 100644 --- a/apps/nestjs-backend/src/features/auth/local-auth/local-auth.service.ts +++ b/apps/nestjs-backend/src/features/auth/local-auth/local-auth.service.ts @@ -10,7 +10,7 @@ import { import { JwtService } from '@nestjs/jwt'; import { generateUserId, getRandomString, HttpErrorCode, RandomType } from '@teable/core'; import { PrismaService } from '@teable/db-main-prisma'; -import { MailTransporterType, MailType } from '@teable/openapi'; +import { EmailVerifyCodeType, MailTransporterType, MailType } from '@teable/openapi'; import type { IChangePasswordRo, IInviteWaitlistVo, ISignup } from '@teable/openapi'; import * as bcrypt from 'bcrypt'; import { isEmpty } from 'lodash'; @@ -285,10 +285,6 @@ export class LocalAuthService { const code = getRandomString(4, RandomType.Number); const token = await this.jwtSignupCode(email, code); - if (this.baseConfig.enableEmailCodeConsole) { - console.info('Signup Verification code: ', '\x1b[34m' + code + '\x1b[0m'); - } - const user = await this.userService.getUserByEmail(email); this.isRegisteredValidate(user); @@ -298,8 +294,9 @@ export class LocalAuthService { ); const emailOptions = await this.mailSenderService.sendEmailVerifyCodeEmailOptions({ - title: 'Signup verification', - message: `Your verification code is ${code}, expires in ${this.authConfig.signupVerificationExpiresIn}.`, + code, + expiresIn: this.authConfig.signupVerificationExpiresIn, + type: EmailVerifyCodeType.Signup, }); await this.mailSenderService.sendMail( @@ -482,12 +479,10 @@ export class LocalAuthService { { email, newEmail, code }, { expiresIn: this.baseConfig.emailCodeExpiresIn } ); - if (this.baseConfig.enableEmailCodeConsole) { - console.info('Change Email Verification code: ', '\x1b[34m' + code + '\x1b[0m'); - } const emailOptions = await this.mailSenderService.sendEmailVerifyCodeEmailOptions({ - title: 'Change Email verification', - message: `Your verification code is ${code}, expires in ${this.baseConfig.emailCodeExpiresIn}.`, + code, + expiresIn: this.baseConfig.emailCodeExpiresIn, + type: EmailVerifyCodeType.ChangeEmail, }); await this.mailSenderService.sendMail( { @@ -550,12 +545,12 @@ export class LocalAuthService { for (const item of updateList) { const times = 10; const code = await this.genWaitlistInviteCode(times); - const mailOptions = await this.mailSenderService.commonEmailOptions({ - to: item.email, - title: 'Welcome', - message: `You're off the waitlist!, Here is your invite code: ${code}, it can be used ${times} times`, - buttonUrl: `${this.mailConfig.origin}/auth/signup?inviteCode=${code}`, - buttonText: 'Signup', + const mailOptions = await this.mailSenderService.waitlistInviteEmailOptions({ + email: item.email, + code, + times, + name: 'Guest', + waitlistInviteUrl: `${this.mailConfig.origin}/auth/signup?inviteCode=${code}`, }); res.push({ email: item.email, diff --git a/apps/nestjs-backend/src/features/auth/utils.ts b/apps/nestjs-backend/src/features/auth/utils.ts index 362c6657a0..814ba11c6e 100644 --- a/apps/nestjs-backend/src/features/auth/utils.ts +++ b/apps/nestjs-backend/src/features/auth/utils.ts @@ -5,12 +5,12 @@ import { getPublicFullStorageUrl } from '../attachments/plugins/utils'; export type IPickUserMe = Pick< Prisma.UserGetPayload, - 'id' | 'name' | 'avatar' | 'phone' | 'email' | 'password' | 'notifyMeta' | 'isAdmin' + 'id' | 'name' | 'avatar' | 'phone' | 'email' | 'password' | 'notifyMeta' | 'isAdmin' | 'lang' >; export const pickUserMe = (user: IPickUserMe): IUserMeVo => { return { - ...pick(user, 'id', 'name', 'phone', 'email', 'isAdmin'), + ...pick(user, 'id', 'name', 'phone', 'email', 'isAdmin', 'lang'), notifyMeta: typeof user.notifyMeta === 'object' ? user.notifyMeta : JSON.parse(user.notifyMeta), avatar: user.avatar && !user.avatar?.startsWith('http') diff --git a/apps/nestjs-backend/src/features/base/base-export.service.ts b/apps/nestjs-backend/src/features/base/base-export.service.ts index 409d1b7146..ffe9e124fa 100644 --- a/apps/nestjs-backend/src/features/base/base-export.service.ts +++ b/apps/nestjs-backend/src/features/base/base-export.service.ts @@ -1,7 +1,7 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { Readable, PassThrough } from 'stream'; import { Injectable, Logger } from '@nestjs/common'; -import type { ILinkFieldOptions } from '@teable/core'; +import type { ILinkFieldOptions, ILocalization } from '@teable/core'; import { FieldType, getRandomString, ViewType, isLinkLookupOptions } from '@teable/core'; import type { Field, View, TableMeta, Base } from '@teable/db-main-prisma'; import { PrismaService } from '@teable/db-main-prisma'; @@ -19,6 +19,7 @@ import { IDbProvider } from '../../db-provider/db.provider.interface'; import { EventEmitterService } from '../../event-emitter/event-emitter.service'; import { Events } from '../../event-emitter/events'; import type { IClsStore } from '../../types/cls'; +import type { I18nPath } from '../../types/i18n.generated'; import { second } from '../../utils/second'; import StorageAdapter from '../attachments/plugins/adapter'; import { InjectStorageAdapter } from '../attachments/plugins/storage'; @@ -26,7 +27,6 @@ import { createFieldInstanceByRaw } from '../field/model/factory'; import { NotificationService } from '../notification/notification.service'; import { createViewVoByRaw } from '../view/model/factory'; import { EXCLUDE_SYSTEM_FIELDS } from './constant'; - @Injectable() export class BaseExportService { public static CSV_CHUNK = 500; @@ -92,13 +92,26 @@ export class BaseExportService { 'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(name)}`, } ); - const messageString = `${baseName} Export successfully: 🗂️ ${name}`; - this.notifyExportResult(baseId, messageString, previewUrl); + const message: ILocalization = { + i18nKey: 'common.email.templates.notify.exportBase.success.message', + context: { + baseName, + previewUrl, + name, + }, + }; + this.notifyExportResult(baseId, message, previewUrl); }) .catch(async (e) => { this.logger.error(`export base zip error: ${e.message}`, e?.stack); - const messageString = `❌ ${baseName} export failed: ${e.message}`; - this.notifyExportResult(baseId, messageString); + const message: ILocalization = { + i18nKey: 'common.email.templates.notify.exportBase.failed.message', + context: { + baseName, + errorMessage: e.message, + }, + }; + this.notifyExportResult(baseId, message); }); } @@ -992,7 +1005,11 @@ export class BaseExportService { }); } - private async notifyExportResult(baseId: string, message: string, previewUrl?: string) { + private async notifyExportResult( + baseId: string, + message: string | ILocalization, + previewUrl?: string + ) { const userId = this.cls.get('user.id'); await this.eventEmitterService.emit(Events.BASE_EXPORT_COMPLETE, { previewUrl, diff --git a/apps/nestjs-backend/src/features/comment/comment-open-api.service.ts b/apps/nestjs-backend/src/features/comment/comment-open-api.service.ts index 5142e40eae..8fa2c63ed4 100644 --- a/apps/nestjs-backend/src/features/comment/comment-open-api.service.ts +++ b/apps/nestjs-backend/src/features/comment/comment-open-api.service.ts @@ -5,6 +5,7 @@ import { BadGatewayException, BadRequestException, } from '@nestjs/common'; +import type { ILocalization } from '@teable/core'; import { generateCommentId, getCommentChannel, getTableCommentChannel } from '@teable/core'; import { PrismaService } from '@teable/db-main-prisma'; import type { @@ -23,6 +24,7 @@ import { ClsService } from 'nestjs-cls'; import { CacheService } from '../../cache/cache.service'; import { ShareDbService } from '../../share-db/share-db.service'; import type { IClsStore } from '../../types/cls'; +import type { I18nPath } from '../../types/i18n.generated'; import { AttachmentsStorageService } from '../attachments/attachments-storage.service'; import StorageAdapter from '../attachments/plugins/adapter'; import { getPublicFullStorageUrl } from '../attachments/plugins/utils'; @@ -653,15 +655,14 @@ export class CommentOpenApiService { return; } - const { name: baseName } = - (await this.prismaService.base.findFirst({ - where: { - id: baseId, - }, - select: { - name: true, - }, - })) || {}; + const { name: baseName } = await this.prismaService.base.findUniqueOrThrow({ + where: { + id: baseId, + }, + select: { + name: true, + }, + }); const recordName = await this.recordService.getCellValue(tableId, recordId, fieldId); @@ -679,7 +680,10 @@ export class CommentOpenApiService { new Set([...notifyUsers.map(({ createdBy }) => createdBy), ...relativeUsers]) ).filter((userId) => userId !== fromUserId); - const message = `${fromUserName} made a commented on ${recordName ? recordName : 'a record'} in ${tableName} ${baseName ? `in ${baseName}` : ''}`; + const message: ILocalization = { + i18nKey: 'common.email.templates.notify.recordComment.message', + context: { fromUserName, recordName, tableName, baseName }, + }; subscribeUsersIds.forEach((userId) => { this.notificationService.sendCommentNotify({ diff --git a/apps/nestjs-backend/src/features/import/open-api/import-csv-chunk.processor.ts b/apps/nestjs-backend/src/features/import/open-api/import-csv-chunk.processor.ts index 60330d3418..8cd5c01345 100644 --- a/apps/nestjs-backend/src/features/import/open-api/import-csv-chunk.processor.ts +++ b/apps/nestjs-backend/src/features/import/open-api/import-csv-chunk.processor.ts @@ -3,13 +3,14 @@ import { Readable } from 'stream'; import { Worker } from 'worker_threads'; import { InjectQueue, OnWorkerEvent, Processor, WorkerHost } from '@nestjs/bullmq'; import { Injectable, Logger } from '@nestjs/common'; -import type { FieldType } from '@teable/core'; +import type { FieldType, ILocalization } from '@teable/core'; import { getRandomString } from '@teable/core'; import { UploadType } from '@teable/openapi'; import type { IImportOptionRo, IImportColumn } from '@teable/openapi'; import { Job, Queue } from 'bullmq'; import Papa from 'papaparse'; import { EventEmitterService } from '../../../event-emitter/event-emitter.service'; +import type { I18nPath } from '../../../types/i18n.generated'; import StorageAdapter from '../../attachments/plugins/adapter'; import { InjectStorageAdapter } from '../../attachments/plugins/storage'; import { NotificationService } from '../../notification/notification.service'; @@ -82,12 +83,22 @@ export class ImportTableCsvChunkQueueProcessor extends WorkerHost { await this.resolveDataByWorker(job); this.logger.log(`import data to ${table.id} chunk data job completed`); } catch (error) { - let finalMessage = ''; + let finalMessage: string | ILocalization = ''; if (error instanceof ImportError && error.range) { const range = error.range; - finalMessage = `❌ ${table.name} import aborted: ${error.message} fail row range: [${range[0]}, ${range[1]}]. Please check the data for this range and retry`; + finalMessage = { + i18nKey: 'common.email.templates.notify.import.table.aborted.message', + context: { + tableName: table.name, + errorMessage: error.message, + range: `${range[0]}, ${range[1]}`, + }, + }; } else if (error instanceof Error) { - finalMessage = `❌ ${table.name} import failed: ${error.message}`; + finalMessage = { + i18nKey: 'common.email.templates.notify.import.table.failed.message', + context: { tableName: table.name, errorMessage: error.message }, + }; } if (notification && finalMessage) { diff --git a/apps/nestjs-backend/src/features/import/open-api/import-csv.processor.ts b/apps/nestjs-backend/src/features/import/open-api/import-csv.processor.ts index 26882bfc6c..8976521e6d 100644 --- a/apps/nestjs-backend/src/features/import/open-api/import-csv.processor.ts +++ b/apps/nestjs-backend/src/features/import/open-api/import-csv.processor.ts @@ -78,7 +78,15 @@ export class ImportTableCsvQueueProcessor extends WorkerHost { baseId, tableId: table.id, toUserId: userId, - message: `🎉 ${table.name} ${sourceColumnMap ? 'inplace' : ''} imported successfully`, + message: sourceColumnMap + ? { + i18nKey: 'common.email.templates.notify.import.table.success.inplace', + context: { tableName: table.name }, + } + : { + i18nKey: 'common.email.templates.notify.import.table.success.message', + context: { tableName: table.name }, + }, }); this.eventEmitterService.emitAsync(Events.IMPORT_TABLE_COMPLETE, { @@ -107,7 +115,14 @@ export class ImportTableCsvQueueProcessor extends WorkerHost { baseId, tableId: table.id, toUserId: userId, - message: `❌ ${table.name} import aborted: ${err.message} fail row range: [${range}]. Please check the data for this range and retry.`, + message: { + i18nKey: 'common.email.templates.notify.import.table.aborted.message', + context: { + tableName: table.name, + errorMessage: err.message, + range: `${range[0]}, ${range[1]}`, + }, + }, }); throw err; diff --git a/apps/nestjs-backend/src/features/import/open-api/import-open-api.service.ts b/apps/nestjs-backend/src/features/import/open-api/import-open-api.service.ts index 52a1c570b1..ef507b01c7 100644 --- a/apps/nestjs-backend/src/features/import/open-api/import-open-api.service.ts +++ b/apps/nestjs-backend/src/features/import/open-api/import-open-api.service.ts @@ -10,7 +10,6 @@ import type { } from '@teable/openapi'; import { chunk, difference } from 'lodash'; import { ClsService } from 'nestjs-cls'; - import { ShareDbService } from '../../../share-db/share-db.service'; import type { IClsStore } from '../../../types/cls'; import { FieldOpenApiService } from '../../field/open-api/field-open-api.service'; diff --git a/apps/nestjs-backend/src/features/mail-sender/mail-sender.service.ts b/apps/nestjs-backend/src/features/mail-sender/mail-sender.service.ts index ba80729862..2fbc16d70a 100644 --- a/apps/nestjs-backend/src/features/mail-sender/mail-sender.service.ts +++ b/apps/nestjs-backend/src/features/mail-sender/mail-sender.service.ts @@ -1,15 +1,25 @@ +/* eslint-disable sonarjs/no-duplicate-string */ import { Injectable, Logger } from '@nestjs/common'; import { MailerService } from '@nestjs-modules/mailer'; import { HttpErrorCode } from '@teable/core'; import type { IMailTransportConfig } from '@teable/openapi'; -import { MailType, CollaboratorType, SettingKey, MailTransporterType } from '@teable/openapi'; +import { + MailType, + CollaboratorType, + SettingKey, + MailTransporterType, + EmailVerifyCodeType, +} from '@teable/openapi'; import { isString } from 'lodash'; +import { I18nService } from 'nestjs-i18n'; import { createTransport } from 'nodemailer'; import { CacheService } from '../../cache/cache.service'; +import { BaseConfig, IBaseConfig } from '../../configs/base.config'; import { IMailConfig, MailConfig } from '../../configs/mail.config'; import { CustomHttpException } from '../../custom.exception'; import { EventEmitterService } from '../../event-emitter/event-emitter.service'; import { Events } from '../../event-emitter/events'; +import type { I18nTranslations } from '../../types/i18n.generated'; import { SettingOpenApiService } from '../setting/open-api/setting-open-api.service'; import { buildEmailFrom, type ISendMailOptions } from './mail-helpers'; @@ -21,9 +31,11 @@ export class MailSenderService { constructor( private readonly mailService: MailerService, @MailConfig() private readonly mailConfig: IMailConfig, + @BaseConfig() private readonly baseConfig: IBaseConfig, private readonly settingOpenApiService: SettingOpenApiService, private readonly eventEmitterService: EventEmitterService, - private readonly cacheService: CacheService + private readonly cacheService: CacheService, + private readonly i18n: I18nService ) { const { host, port, secure, auth, sender, senderName } = this.mailConfig; this.defaultTransportConfig = { @@ -111,7 +123,9 @@ export class MailSenderService { async notifyMergeOptions(list: ISendMailOptions & { mailType: MailType }[], brandName: string) { return { - subject: `Notify - ${brandName}`, + subject: this.i18n.t('common.email.templates.notify.subject', { + args: { brandName }, + }), template: 'normal', context: { partialBody: 'notify-merge-body', @@ -194,7 +208,9 @@ export class MailSenderService { const resourceAlias = resourceType === CollaboratorType.Space ? 'Space' : 'Base'; return { - subject: `${name} (${email}) invited you to their ${resourceAlias} ${resourceName} - ${brandName}`, + subject: this.i18n.t('common.email.templates.invite.subject', { + args: { name, email, resourceAlias, resourceName, brandName }, + }), template: 'normal', context: { name, @@ -204,6 +220,11 @@ export class MailSenderService { inviteUrl, partialBody: 'invite', brandName, + title: this.i18n.t('common.email.templates.invite.title'), + message: this.i18n.t('common.email.templates.invite.message', { + args: { name, email, resourceAlias, resourceName }, + }), + buttonText: this.i18n.t('common.email.templates.invite.buttonText'), }, }; } @@ -230,10 +251,14 @@ export class MailSenderService { const viewRecordUrlPrefix = `${this.mailConfig.origin}/base/${baseId}/${tableId}`; const { brandName } = await this.settingOpenApiService.getServerBrand(); if (refLength <= 1) { - subject = `${fromUserName} added you to the ${fieldName} field of a record in ${tableName}`; + subject = this.i18n.t('common.email.templates.collaboratorCellTag.subject', { + args: { fromUserName, fieldName, tableName }, + }); partialBody = 'collaborator-cell-tag'; } else { - subject = `${fromUserName} added you to ${refLength} records in ${tableName}`; + subject = this.i18n.t('common.email.templates.collaboratorMultiRowTag.subject', { + args: { fromUserName, refLength, tableName }, + }); partialBody = 'collaborator-multi-row-tag'; } @@ -251,6 +276,10 @@ export class MailSenderService { viewRecordUrlPrefix, partialBody, brandName, + title: this.i18n.t('common.email.templates.collaboratorCellTag.title', { + args: { fromUserName, fieldName, tableName }, + }), + buttonText: this.i18n.t('common.email.templates.collaboratorCellTag.buttonText'), }, }; } @@ -297,32 +326,181 @@ export class MailSenderService { }; } - async resetPasswordEmailOptions(info: { name: string; email: string; resetPasswordUrl: string }) { - const { name, email, resetPasswordUrl } = info; + async sendTestEmailOptions(info: { message?: string }) { + const { message } = info; const { brandName } = await this.settingOpenApiService.getServerBrand(); return { - subject: `Reset your password - ${brandName}`, + subject: this.i18n.t('common.email.templates.test.subject', { + args: { brandName }, + }), template: 'normal', context: { - name, - email, - resetPasswordUrl, + partialBody: 'html-body', + brandName, + title: this.i18n.t('common.email.templates.test.title'), + message: message || this.i18n.t('common.email.templates.test.message'), + }, + }; + } + + async waitlistInviteEmailOptions(info: { + code: string; + times: number; + name: string; + email: string; + waitlistInviteUrl: string; + }) { + const { code, times, name, email, waitlistInviteUrl } = info; + const { brandName } = await this.settingOpenApiService.getServerBrand(); + return { + subject: this.i18n.t('common.email.templates.waitlistInvite.subject', { + args: { name, email, brandName }, + }), + template: 'normal', + context: { + ...info, + partialBody: 'common-body', brandName, + title: this.i18n.t('common.email.templates.waitlistInvite.title'), + message: this.i18n.t('common.email.templates.waitlistInvite.message', { + args: { brandName, code, times }, + }), + buttonText: this.i18n.t('common.email.templates.waitlistInvite.buttonText'), + buttonUrl: waitlistInviteUrl, + }, + }; + } + + async resetPasswordEmailOptions(info: { name: string; email: string; resetPasswordUrl: string }) { + const { resetPasswordUrl } = info; + const { brandName } = await this.settingOpenApiService.getServerBrand(); + + return { + subject: this.i18n.t('common.email.templates.resetPassword.subject', { + args: { + brandName, + }, + }), + template: 'normal', + context: { partialBody: 'reset-password', + brandName, + title: this.i18n.t('common.email.templates.resetPassword.title'), + message: this.i18n.t('common.email.templates.resetPassword.message'), + buttonText: this.i18n.t('common.email.templates.resetPassword.buttonText'), + buttonUrl: resetPasswordUrl, + }, + }; + } + + async sendEmailVerifyCodeEmailOptions( + payload: + | { + code: string; + expiresIn: string; + type: EmailVerifyCodeType.Signup | EmailVerifyCodeType.ChangeEmail; + } + | { + domain: string; + name: string; + code: string; + expiresIn: string; + type: EmailVerifyCodeType.DomainVerification; + } + ) { + const { type, code, expiresIn } = payload; + if (this.baseConfig.enableEmailCodeConsole) { + this.logger.log(`${type} Verification code: ${code} expiresIn ${expiresIn}`); + } + switch (type) { + case EmailVerifyCodeType.Signup: + return this.sendSignupVerificationEmailOptions(payload); + case EmailVerifyCodeType.ChangeEmail: + return this.sendChangeEmailCodeEmailOptions(payload); + case EmailVerifyCodeType.DomainVerification: + return this.sendDomainVerificationEmailOptions(payload); + } + } + + private async sendSignupVerificationEmailOptions(payload: { code: string; expiresIn: string }) { + const { code, expiresIn } = payload; + const { brandName } = await this.settingOpenApiService.getServerBrand(); + return { + subject: this.i18n.t('common.email.templates.emailVerifyCode.signupVerification.subject', { + args: { + brandName, + }, + }), + template: 'normal', + context: { + partialBody: 'email-verify-code', + brandName, + title: this.i18n.t('common.email.templates.emailVerifyCode.signupVerification.title'), + message: this.i18n.t('common.email.templates.emailVerifyCode.signupVerification.message', { + args: { + code, + expiresIn: parseInt(expiresIn), + }, + }), }, }; } - async sendEmailVerifyCodeEmailOptions(info: { title: string; message: string }) { - const { title } = info; + private async sendChangeEmailCodeEmailOptions(payload: { code: string; expiresIn: string }) { + const { code, expiresIn } = payload; const { brandName } = await this.settingOpenApiService.getServerBrand(); return { - subject: `${title} - ${brandName}`, + subject: this.i18n.t( + 'common.email.templates.emailVerifyCode.changeEmailVerification.subject', + { + args: { brandName }, + } + ), template: 'normal', context: { partialBody: 'email-verify-code', brandName, - ...info, + title: this.i18n.t('common.email.templates.emailVerifyCode.changeEmailVerification.title'), + message: this.i18n.t( + 'common.email.templates.emailVerifyCode.changeEmailVerification.message', + { + args: { + code, + expiresIn: parseInt(expiresIn), + }, + } + ), + }, + }; + } + + private async sendDomainVerificationEmailOptions(payload: { + domain: string; + name: string; + code: string; + expiresIn: string; + }) { + const { domain, name, code, expiresIn } = payload; + const { brandName } = await this.settingOpenApiService.getServerBrand(); + return { + subject: this.i18n.t('common.email.templates.emailVerifyCode.domainVerification.subject', { + args: { + brandName, + }, + }), + template: 'normal', + context: { + partialBody: 'email-verify-code', + brandName, + title: this.i18n.t('common.email.templates.emailVerifyCode.domainVerification.title', { + args: { domain, name }, + }), + message: this.i18n.t('common.email.templates.emailVerifyCode.domainVerification.message', { + args: { + code, + expiresIn: parseInt(expiresIn), + }, + }), }, }; } diff --git a/apps/nestjs-backend/src/features/mail-sender/open-api/mail-sender-open-api.service.ts b/apps/nestjs-backend/src/features/mail-sender/open-api/mail-sender-open-api.service.ts index a57813a2b2..2f8eec259d 100644 --- a/apps/nestjs-backend/src/features/mail-sender/open-api/mail-sender-open-api.service.ts +++ b/apps/nestjs-backend/src/features/mail-sender/open-api/mail-sender-open-api.service.ts @@ -16,13 +16,7 @@ export class MailSenderOpenApiService { const transport = createTransport(transportConfig); await transport.verify(); - const option = await this.mailSenderService.htmlEmailOptions({ - to, - title: 'Test', - message: message || 'This is a test email from Teable', - buttonUrl: this.mailConfig.origin, - buttonText: 'Teable', - }); + const option = await this.mailSenderService.sendTestEmailOptions({ message }); await this.mailSenderService.sendMailByConfig({ to, ...option }, transportConfig); } } diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-cell-tag.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-cell-tag.hbs index f574cac75e..4452895e8e 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-cell-tag.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-cell-tag.hbs @@ -1,15 +1,12 @@ - -

- {{fromUserName}} added you to the {{fieldName}} field of a - record - in - {{tableName}}. -

- View record - - - + ' + >{{buttonText}} + + + \ No newline at end of file diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-multi-row-tag.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-multi-row-tag.hbs index 9db03778e4..3764c72ee3 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-multi-row-tag.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/collaborator-multi-row-tag.hbs @@ -1,19 +1,38 @@ -

- {{fromUserName}} added you to {{refLength}} records in - {{tableName}}: +

+ {{{title}}}:

- {{#each recordIds}} - The {{../fieldName}} field of a record: - + {{#each recordIds}} + {{../viewRecordUrlPrefix}}?recordId={{this}}&fromNotify={{../notifyId}} - -
-
- {{/each}} + target="_blank" + style=" + display: inline-block; + width: calc(50% - 24px); + padding: 6px 8px; + margin: 6px 8px; + background-color: #f4f4f5; + color: #3276dc; + border-radius: 6px; + text-decoration: none; + font-weight: 500; + font-size: 14px; + border: 1px solid #e4e4e7; + transition: background-color 0.2s ease; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + box-sizing: border-box; + vertical-align: top; + " + onmouseover="this.style.backgroundColor='#e4e4e7'" + onmouseout="this.style.backgroundColor='#f4f4f5'" + title="{{this}}" + >{{this}} + {{/each}} + diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/common-body.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/common-body.hbs index 58761700af..abd7e60223 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/common-body.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/common-body.hbs @@ -1,7 +1,11 @@ - -

{{title}}

-

{{message}}

- {{buttonText}} - - + +

{{{title}}}

+

{{{message}}}

+ {{buttonText}} + + \ No newline at end of file diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/email-verify-code.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/email-verify-code.hbs index a1b42bb5b8..7a034cca77 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/email-verify-code.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/email-verify-code.hbs @@ -1,6 +1,6 @@ - -

{{title}}

-

{{message}}

- - + +

{{{title}}}

+

{{{message}}}

+ + \ No newline at end of file diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/invite.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/invite.hbs index 05ce9af287..5520f8310a 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/invite.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/invite.hbs @@ -1,11 +1,13 @@ - -

Invitation to Collaborate

-

{{name}} ({{email}}) has invited you to - collaborate - on their {{resourceAlias}} {{resourceName}}.

- Accept - Invitation - - + +

{{{title}}}

+

+ {{{message}}} +

+ {{buttonText}} + + \ No newline at end of file diff --git a/apps/nestjs-backend/src/features/mail-sender/templates/partials/reset-password.hbs b/apps/nestjs-backend/src/features/mail-sender/templates/partials/reset-password.hbs index e18da8db58..abd7e60223 100644 --- a/apps/nestjs-backend/src/features/mail-sender/templates/partials/reset-password.hbs +++ b/apps/nestjs-backend/src/features/mail-sender/templates/partials/reset-password.hbs @@ -1,7 +1,11 @@ - -

Reset Your Password

-

If you did not request this change, please ignore this email. Otherwise, click the button below to reset your password.

- Reset Password - - + +

{{{title}}}

+

{{{message}}}

+ {{buttonText}} + + \ No newline at end of file diff --git a/apps/nestjs-backend/src/features/model/setting.ts b/apps/nestjs-backend/src/features/model/setting.ts index 763a67496f..93faef5fe8 100644 --- a/apps/nestjs-backend/src/features/model/setting.ts +++ b/apps/nestjs-backend/src/features/model/setting.ts @@ -37,6 +37,6 @@ export class SettingModel { statsType: 'instance:setting', }) async getSetting() { - return await this.prismaService.txClient().setting.findMany(); + return await this.prismaService.setting.findMany(); } } diff --git a/apps/nestjs-backend/src/features/notification/notification.service.ts b/apps/nestjs-backend/src/features/notification/notification.service.ts index 582545c8ff..ae3bc9a772 100644 --- a/apps/nestjs-backend/src/features/notification/notification.service.ts +++ b/apps/nestjs-backend/src/features/notification/notification.service.ts @@ -1,5 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; -import type { INotificationBuffer, INotificationUrl } from '@teable/core'; +import type { ILocalization, INotificationBuffer, INotificationUrl } from '@teable/core'; import { generateNotificationId, getUserNotificationChannel, @@ -21,8 +21,10 @@ import { type IUpdateNotifyStatusRo, } from '@teable/openapi'; import { keyBy } from 'lodash'; +import { I18nContext, I18nService } from 'nestjs-i18n'; import { IMailConfig, MailConfig } from '../../configs/mail.config'; import { ShareDbService } from '../../share-db/share-db.service'; +import type { I18nPath, I18nTranslations } from '../../types/i18n.generated'; import { getPublicFullStorageUrl } from '../attachments/plugins/utils'; import { MailSenderService } from '../mail-sender/mail-sender.service'; import { UserService } from '../user/user.service'; @@ -42,9 +44,38 @@ export class NotificationService { private readonly shareDbService: ShareDbService, private readonly mailSenderService: MailSenderService, private readonly userService: UserService, - @MailConfig() private readonly mailConfig: IMailConfig + @MailConfig() private readonly mailConfig: IMailConfig, + private readonly i18n: I18nService ) {} + private async getUserLang(userId: string) { + const user = await this.userService.getUserById(userId); + return user?.lang ?? I18nContext.current()?.lang; + } + + private getMessage(text: string | ILocalization, lang?: string) { + return typeof text === 'string' + ? text + : (this.i18n.t(text.i18nKey, { + args: text.context, + lang: lang ?? I18nContext.current()?.lang, + }) as string); + } + + /** + * notification message i18n use common prefix, so we need to remove it to save db + */ + private getMessageI18n(localization: string | ILocalization) { + return typeof localization === 'string' + ? undefined + : JSON.stringify({ + // remove common prefix + // eg: common.email.templates -> email.templates + i18nKey: localization.i18nKey.replace(/^common\./, ''), + context: localization.context, + }); + } + async sendCollaboratorNotify(params: { fromUserId: string; toUserId: string; @@ -67,11 +98,6 @@ export class NotificationService { } const notifyId = generateNotificationId(); - const emailOptions = await this.mailSenderService.collaboratorCellTagEmailOptions({ - notifyId, - fromUserName: fromUser.name, - refRecord, - }); const userIcon = userIconSchema.parse({ userId: fromUser.id, @@ -91,12 +117,33 @@ export class NotificationService { const notifyPath = this.generateNotifyPath(type as NotificationTypeEnum, urlMeta); + let message: string | ILocalization = ''; + if (refRecord.recordIds.length <= 1) { + message = { + i18nKey: 'common.email.templates.collaboratorCellTag.subject', + context: { + fromUserName: fromUser.name, + fieldName: refRecord.fieldName, + tableName: refRecord.tableName, + }, + }; + } else { + message = { + i18nKey: 'common.email.templates.collaboratorMultiRowTag.subject', + context: { + fromUserName: fromUser.name, + refLength: refRecord.recordIds.length.toString(), + tableName: refRecord.tableName, + }, + }; + } const data: Prisma.NotificationCreateInput = { id: notifyId, fromUserId, toUserId, type, - message: emailOptions.notifyMessage, + message: this.getMessage(message, 'en'), + messageI18n: this.getMessageI18n(message), urlPath: notifyPath, createdBy: fromUserId, }; @@ -108,6 +155,7 @@ export class NotificationService { notification: { id: notifyData.id, message: notifyData.message, + messageI18n: notifyData.messageI18n, notifyIcon: userIcon, notifyType: notifyData.type as NotificationTypeEnum, url: this.mailConfig.origin + notifyPath, @@ -119,6 +167,11 @@ export class NotificationService { this.sendNotifyBySocket(toUser.id, socketNotification); + const emailOptions = await this.mailSenderService.collaboratorCellTagEmailOptions({ + notifyId, + fromUserName: fromUser.name, + refRecord, + }); if (toUser.notifyMeta && toUser.notifyMeta.email) { this.mailSenderService.sendMail( { @@ -138,17 +191,17 @@ export class NotificationService { path: string; fromUserId?: string; toUserId: string; - message: string; + message: string | ILocalization; emailConfig?: { - title: string; - message: string; + title: string | ILocalization; + message: string | ILocalization; buttonUrl?: string; - buttonText?: string; + buttonText?: string | ILocalization; }; }, type = NotificationTypeEnum.System ) { - const { toUserId, emailConfig, message, path, fromUserId = SYSTEM_USER_ID } = params; + const { toUserId, emailConfig, path, fromUserId = SYSTEM_USER_ID } = params; const notifyId = generateNotificationId(); const toUser = await this.userService.getUserById(toUserId); if (!toUser) { @@ -162,7 +215,8 @@ export class NotificationService { type, urlPath: path, createdBy: fromUserId, - message, + message: this.getMessage(params.message, 'en'), + messageI18n: this.getMessageI18n(params.message), }; const notifyData = await this.createNotify(data); @@ -184,6 +238,7 @@ export class NotificationService { notification: { id: notifyData.id, message: notifyData.message, + messageI18n: notifyData.messageI18n, notifyType: type, url: path, notifyIcon: systemNotifyIcon, @@ -196,11 +251,16 @@ export class NotificationService { this.sendNotifyBySocket(toUser.id, socketNotification); if (emailConfig && toUser.notifyMeta && toUser.notifyMeta.email) { + const lang = await this.getUserLang(toUserId); const emailOptions = await this.mailSenderService.htmlEmailOptions({ ...emailConfig, + title: this.getMessage(emailConfig.title, lang), + message: this.getMessage(emailConfig.message, lang), to: toUserId, buttonUrl: emailConfig.buttonUrl || this.mailConfig.origin + path, - buttonText: emailConfig.buttonText || 'View', + buttonText: emailConfig.buttonText + ? this.getMessage(emailConfig.buttonText, lang) + : this.i18n.t('common.email.templates.notify.buttonText'), }); this.mailSenderService.sendMail( { @@ -220,17 +280,17 @@ export class NotificationService { path: string; fromUserId?: string; toUserId: string; - message: string; + message: string | ILocalization; emailConfig?: { - title: string; - message: string; + title: string | ILocalization; + message: string | ILocalization; buttonUrl?: string; // use path as default - buttonText?: string; // use 'View' as default + buttonText?: string | ILocalization; // use 'View' as default }; }, type = NotificationTypeEnum.System ) { - const { toUserId, emailConfig, message, path, fromUserId = SYSTEM_USER_ID } = params; + const { toUserId, emailConfig, path, fromUserId = SYSTEM_USER_ID } = params; const notifyId = generateNotificationId(); const toUser = await this.userService.getUserById(toUserId); if (!toUser) { @@ -244,7 +304,8 @@ export class NotificationService { type, urlPath: path, createdBy: fromUserId, - message, + message: this.getMessage(params.message, 'en'), + messageI18n: this.getMessageI18n(params.message), }; const notifyData = await this.createNotify(data); @@ -266,6 +327,7 @@ export class NotificationService { notification: { id: notifyData.id, message: notifyData.message, + messageI18n: notifyData.messageI18n, notifyType: type, url: path, notifyIcon: systemNotifyIcon, @@ -278,11 +340,16 @@ export class NotificationService { this.sendNotifyBySocket(toUser.id, socketNotification); if (emailConfig && toUser.notifyMeta && toUser.notifyMeta.email) { + const lang = await this.getUserLang(toUserId); const emailOptions = await this.mailSenderService.commonEmailOptions({ ...emailConfig, + title: this.getMessage(emailConfig.title, lang), + message: this.getMessage(emailConfig.message, lang), to: toUserId, buttonUrl: emailConfig.buttonUrl || this.mailConfig.origin + path, - buttonText: emailConfig.buttonText || 'View', + buttonText: emailConfig.buttonText + ? this.getMessage(emailConfig.buttonText, lang) + : this.i18n.t('common.email.templates.notify.buttonText'), }); this.mailSenderService.sendMail( { @@ -301,7 +368,7 @@ export class NotificationService { tableId: string; baseId: string; toUserId: string; - message: string; + message: string | ILocalization; }) { const { toUserId, tableId, message, baseId } = params; const toUser = await this.userService.getUserById(toUserId); @@ -320,13 +387,17 @@ export class NotificationService { toUserId, message, emailConfig: { - title: 'Import result notification', - message: message, + title: { i18nKey: 'common.email.templates.notify.import.title' }, + message, }, }); } - async sendExportBaseResultNotify(params: { baseId: string; toUserId: string; message: string }) { + async sendExportBaseResultNotify(params: { + baseId: string; + toUserId: string; + message: string | ILocalization; + }) { const { toUserId, message } = params; const toUser = await this.userService.getUserById(toUserId); if (!toUser) { @@ -340,7 +411,7 @@ export class NotificationService { toUserId, message, emailConfig: { - title: 'Export base result notification', + title: { i18nKey: 'common.email.templates.notify.exportBase.title' }, message: message, }, }, @@ -354,7 +425,7 @@ export class NotificationService { recordId: string; commentId: string; toUserId: string; - message: string; + message: string | ILocalization; fromUserId: string; }) { const { toUserId, tableId, message, baseId, commentId, recordId, fromUserId } = params; @@ -378,7 +449,7 @@ export class NotificationService { toUserId, message, emailConfig: { - title: 'Record comment notification', + title: { i18nKey: 'common.email.templates.notify.recordComment.title' }, message: message, }, }, @@ -422,6 +493,7 @@ export class NotificationService { notifyType: v.type as NotificationTypeEnum, url: this.mailConfig.origin + v.urlPath, message: v.message, + messageI18n: v.messageI18n, isRead: v.isRead, createdTime: v.createdTime.toISOString(), }; diff --git a/apps/nestjs-backend/src/features/user/user.controller.ts b/apps/nestjs-backend/src/features/user/user.controller.ts index 133fbc50c4..1bc5d561e9 100644 --- a/apps/nestjs-backend/src/features/user/user.controller.ts +++ b/apps/nestjs-backend/src/features/user/user.controller.ts @@ -8,8 +8,10 @@ import { } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { + IUpdateUserLangRo, IUpdateUserNameRo, IUserNotifyMeta, + updateUserLangRoSchema, updateUserNameRoSchema, userNotifyMetaSchema, } from '@teable/openapi'; @@ -61,4 +63,12 @@ export class UserController { const userId = this.cls.get('user.id'); return this.userService.updateNotifyMeta(userId, updateUserNotifyMetaRo); } + + @Patch('lang') + async updateLang( + @Body(new ZodValidationPipe(updateUserLangRoSchema)) updateUserLangRo: IUpdateUserLangRo + ): Promise { + const userId = this.cls.get('user.id'); + return this.userService.updateLang(userId, updateUserLangRo.lang); + } } diff --git a/apps/nestjs-backend/src/features/user/user.service.ts b/apps/nestjs-backend/src/features/user/user.service.ts index 826bf5745d..ca6c9fb601 100644 --- a/apps/nestjs-backend/src/features/user/user.service.ts +++ b/apps/nestjs-backend/src/features/user/user.service.ts @@ -13,6 +13,7 @@ import { PrismaService } from '@teable/db-main-prisma'; import { CollaboratorType, PrincipalType, UploadType } from '@teable/openapi'; import type { IUserInfoVo, ICreateSpaceRo, IUserNotifyMeta } from '@teable/openapi'; import { ClsService } from 'nestjs-cls'; +import { I18nContext } from 'nestjs-i18n'; import sharp from 'sharp'; import { CacheService } from '../../cache/cache.service'; import { BaseConfig, IBaseConfig } from '../../configs/base.config'; @@ -154,6 +155,7 @@ export class UserService { ...user, name: user.name ?? user.email.split('@')[0], isAdmin: isAdmin ? true : null, + lang: I18nContext.current()?.lang, }, }); const { id, name } = newUser; @@ -238,6 +240,15 @@ export class UserService { }); } + async updateLang(id: string, lang: string) { + await this.prismaService.txClient().user.update({ + data: { + lang, + }, + where: { id, deletedTime: null }, + }); + } + private async generateDefaultAvatar(id: string) { const path = join(StorageAdapter.getDir(UploadType.Avatar), id); const bucket = StorageAdapter.getBucket(UploadType.Avatar); diff --git a/apps/nestjs-backend/src/global/global.module.ts b/apps/nestjs-backend/src/global/global.module.ts index 6d5ba290fa..882bbc9b1d 100644 --- a/apps/nestjs-backend/src/global/global.module.ts +++ b/apps/nestjs-backend/src/global/global.module.ts @@ -6,6 +6,13 @@ import { PrismaModule } from '@teable/db-main-prisma'; import type { Request } from 'express'; import { nanoid } from 'nanoid'; import { ClsMiddleware, ClsModule } from 'nestjs-cls'; +import { + I18nModule, + QueryResolver, + AcceptLanguageResolver, + HeaderResolver, + CookieResolver, +} from 'nestjs-i18n'; import { CacheModule } from '../cache/cache.module'; import { ConfigModule } from '../configs/config.module'; import { X_REQUEST_ID } from '../const'; @@ -19,6 +26,7 @@ import { ModelModule } from '../features/model/model.module'; import { RequestInfoMiddleware } from '../middleware/request-info.middleware'; import { PerformanceCacheModule } from '../performance-cache'; import { RouteTracingInterceptor } from '../tracing/route-tracing.interceptor'; +import { getI18nPath, getI18nTypesOutputPath } from '../utils/i18n'; import { KnexModule } from './knex'; const globalModules = { @@ -49,7 +57,34 @@ const globalModules = { PermissionModule, DataLoaderModule, PerformanceCacheModule, + I18nModule.forRootAsync({ + useFactory: () => { + const i18nPath = getI18nPath(); + const typesOutputPath = getI18nTypesOutputPath(); + return { + fallbackLanguage: 'en', + loaderOptions: { + path: i18nPath, + watch: process.env.NODE_ENV !== 'production', + }, + typesOutputPath, + formatter: (template: string, ...args: Array>) => { + // replace {{field}} to {$field} + const normalized = template.replace(/\{\{\s*(\w+)\s*\}\}/g, '{$1}'); + const options = I18nModule['sanitizeI18nOptions'](); + return options.formatter(normalized, ...args); + }, + }; + }, + resolvers: [ + { use: QueryResolver, options: ['lang'] }, + { use: CookieResolver, options: ['NEXT_LOCALE'] }, + AcceptLanguageResolver, + new HeaderResolver(['x-lang']), + ], + }), ], + // for overriding the default TablePermissionService, FieldPermissionService, RecordPermissionService, and ViewPermissionService providers: [ DbProvider, diff --git a/apps/nestjs-backend/src/types/i18n.generated.ts b/apps/nestjs-backend/src/types/i18n.generated.ts new file mode 100644 index 0000000000..51e3483683 --- /dev/null +++ b/apps/nestjs-backend/src/types/i18n.generated.ts @@ -0,0 +1,3423 @@ +/* DO NOT EDIT, file generated by nestjs-i18n */ + +/* eslint-disable */ +/* prettier-ignore */ +import { Path } from "nestjs-i18n"; +/* prettier-ignore */ +export type I18nTranslations = { + "auth": { + "page": { + "signin": string; + "signup": string; + "title": string; + }; + "title": { + "signin": string; + "signup": string; + }; + "content": { + "title": string; + "description": string; + }; + "button": { + "signin": string; + "signup": string; + "resend": string; + }; + "label": { + "email": string; + "password": string; + "verificationCode": string; + }; + "placeholder": { + "password": string; + "email": string; + "verificationCode": string; + }; + "signError": { + "exist": string; + "incorrect": string; + "tooManyRequests": string; + "turnstileRequired": string; + "turnstileError": string; + "turnstileExpired": string; + "turnstileTimeout": string; + }; + "signupError": { + "verificationCodeRequired": string; + "verificationCodeInvalid": string; + "passwordLength": string; + "passwordInvalid": string; + "sendMailRateLimit": string; + }; + "socialAuth": { + "title": string; + }; + "resetPassword": { + "header": string; + "description": string; + "label": string; + "error": { + "requiredPassword": string; + "invalidLink": string; + }; + "success": { + "title": string; + "description": string; + }; + "buttonText": string; + }; + "forgetPassword": { + "trigger": string; + "header": string; + "description": string; + "errorRequiredEmail": string; + "errorInvalidEmail": string; + "buttonText": string; + "success": { + "title": string; + "description": string; + }; + "sendMailRateLimit": string; + }; + "legal": { + "tip": string; + "termsUrl": string; + "privacyUrl": string; + }; + }; + "chart": { + "notBaseId": string; + "notPositionId": string; + "notPluginInstallId": string; + "initBridge": string; + "actions": { + "cancel": string; + "save": string; + }; + "queryTitle": string; + "notSupport": string; + "chart": { + "bar": string; + "line": string; + "pie": string; + "area": string; + "table": string; + }; + "form": { + "chartType": { + "placeholder": string; + "label": string; + }; + "pie": { + "dimension": string; + "measure": string; + "showTotal": string; + }; + "combo": { + "xAxis": { + "label": string; + "placeholder": string; + }; + "yAxis": { + "label": string; + "placeholder": string; + "position": string; + }; + "xDisplay": { + "label": string; + }; + "yDisplay": { + "label": string; + }; + "addXAxis": string; + "addYAxis": string; + "stack": string; + "position": { + "label": string; + "auto": string; + "left": string; + "right": string; + }; + "goalLine": { + "label": string; + }; + "range": { + "label": string; + "min": string; + "max": string; + }; + "lineStyle": { + "label": string; + "normal": string; + "linear": string; + "step": string; + }; + "displayType": string; + }; + "typeError": string; + "updateQuery": string; + "queryError": string; + "querySuccess": string; + "decimal": string; + "prefix": string; + "suffix": string; + "showLabel": string; + "showLegend": string; + "value": string; + "label": string; + "padding": { + "label": string; + "top": string; + "right": string; + "bottom": string; + "left": string; + }; + "tableConfig": string; + "width": string; + }; + "reloadQuery": string; + "noStorage": string; + "noPermission": string; + "goConfig": string; + }; + "common": { + "actions": { + "title": string; + "add": string; + "save": string; + "doNotSave": string; + "submit": string; + "confirm": string; + "close": string; + "edit": string; + "fill": string; + "update": string; + "create": string; + "delete": string; + "cancel": string; + "zoomIn": string; + "zoomOut": string; + "back": string; + "remove": string; + "removeConfig": string; + "saveSucceed": string; + "submitSucceed": string; + "editSucceed": string; + "updateSucceed": string; + "deleteSucceed": string; + "resetSucceed": string; + "restoreSucceed": string; + "loading": string; + "refreshPage": string; + "yesDelete": string; + "rename": string; + "duplicate": string; + "change": string; + "upgrade": string; + "search": string; + "loadMore": string; + "collapseSidebar": string; + "restore": string; + "permanentDelete": string; + "globalSearch": string; + "fieldSearch": string; + "tableIndex": string; + "showAllRow": string; + "hideNotMatchRow": string; + "more": string; + "move": string; + "turnOn": string; + "exit": string; + "next": string; + "previous": string; + "continue": string; + "export": string; + "import": string; + "expand": string; + "deleteTip": string; + }; + "quickAction": { + "title": string; + "placeHolder": string; + }; + "password": { + "setInvalid": string; + }; + "template": { + "aiTitle": string; + "aiSubTitle": string; + "guideTitle": string; + "watchVideo": string; + "title": string; + "description": string; + "browseAll": string; + "templateTitle": string; + "loadMore": string; + "allTemplatesLoaded": string; + "createTemplate": string; + "promptBox": { + "placeholder": string; + "start": string; + "carouselGuides": { + "guide1": string; + "guide2": string; + "guide3": string; + "guide4": string; + "guide5": string; + "guide6": string; + "guide7": string; + }; + }; + }; + "settings": { + "title": string; + "back": string; + "account": { + "title": string; + "tab": string; + "updatePhoto": string; + "updateNameDesc": string; + "securityTitle": string; + "email": string; + "password": string; + "passwordDesc": string; + "changePassword": { + "title": string; + "desc": string; + "current": string; + "new": string; + "confirm": string; + }; + "changePasswordError": { + "disMatch": string; + "equal": string; + "invalid": string; + "invalidNew": string; + }; + "changePasswordSuccess": { + "title": string; + "desc": string; + }; + "manageToken": string; + "addPassword": { + "title": string; + "desc": string; + "password": string; + "confirm": string; + }; + "addPasswordError": { + "disMatch": string; + "invalid": string; + }; + "addPasswordSuccess": { + "title": string; + }; + "changeEmail": { + "title": string; + "desc": string; + "current": string; + "new": string; + "code": string; + "getCode": string; + "error": { + "invalidCode": string; + "invalidPassword": string; + "invalidConflict": string; + "invalidSameEmail": string; + "sendMailRateLimit": string; + }; + "success": { + "title": string; + "desc": string; + "sendSuccess": string; + }; + }; + "deleteAccount": { + "title": string; + "desc": string; + "error": { + "title": string; + "desc": string; + "spacesError": string; + }; + "confirm": { + "title": string; + "placeholder": string; + }; + "loading": string; + }; + }; + "notify": { + "title": string; + "label": string; + "desc": string; + }; + "setting": { + "title": string; + "theme": string; + "themeDesc": string; + "dark": string; + "light": string; + "system": string; + "version": string; + "language": string; + "interactionMode": string; + "mouseMode": string; + "touchMode": string; + "systemMode": string; + }; + "nav": { + "settings": string; + "logout": string; + "contactSupport": string; + }; + "integration": { + "title": string; + "description": string; + "lastUsed": string; + "revoke": string; + "owner": string; + "revokeTitle": string; + "revokeDesc": string; + "scopeTitle": string; + "scopeDesc": string; + }; + "templateAdmin": { + "title": string; + "noData": string; + "importing": string; + "usageCount": string; + "useTemplate": string; + "createdBy": string; + "backToTemplateList": string; + "tips": { + "errorCategoryName": string; + "needSnapshot": string; + "needBaseSource": string; + "forbiddenUpdateSystemTemplate": string; + "addCategoryTips": string; + }; + "category": { + "menu": { + "getStarted": string; + "all": string; + "browseByCategory": string; + }; + }; + "header": { + "cover": string; + "name": string; + "description": string; + "markdownDescription": string; + "category": string; + "isSystem": string; + "source": string; + "status": string; + "publishSnapshot": string; + "snapshotTime": string; + "actions": string; + }; + "actions": { + "title": string; + "publish": string; + "delete": string; + "duplicate": string; + "preview": string; + "use": string; + "pinTop": string; + "addCategory": string; + "selectCategory": string; + }; + "baseSelectPanel": { + "title": string; + "description": string; + "confirm": string; + "search": string; + "cancel": string; + "selectBase": string; + "createTemplate": string; + "abnormalBase": string; + }; + }; + }; + "noun": { + "table": string; + "view": string; + "space": string; + "base": string; + "field": string; + "record": string; + "dashboard": string; + "automation": string; + "authorityMatrix": string; + "adminPanel": string; + "license": string; + "instanceId": string; + "beta": string; + "trash": string; + "global": string; + "organizationPanel": string; + "unknownError": string; + "design": string; + "pluginPanel": string; + "pluginContextMenu": string; + "plugin": string; + "copy": string; + "credits": string; + "aiChat": string; + "app": string; + "webSearch": string; + "float": string; + }; + "level": { + "free": string; + "plus": string; + "pro": string; + "enterprise": string; + }; + "noResult": string; + "untitled": string; + "name": string; + "description": string; + "required": string; + "atLeastOne": string; + "guide": { + "prev": string; + "next": string; + "done": string; + "skip": string; + "createSpaceTooltipTitle": string; + "createSpaceTooltipContent": string; + "createBaseTooltipTitle": string; + "createBaseTooltipContent": string; + "createTableTooltipTitle": string; + "createTableTooltipContent": string; + "createViewTooltipTitle": string; + "createViewTooltipContent": string; + "viewFilteringTooltipTitle": string; + "viewFilteringTooltipContent": string; + "viewSortingTooltipTitle": string; + "viewSortingTooltipContent": string; + "viewGroupingTooltipTitle": string; + "viewGroupingTooltipContent": string; + "apiButtonTooltipTitle": string; + "apiButtonTooltipContent": string; + }; + "token": string; + "poweredBy": string; + "invite": { + "dialog": { + "title": string; + "desc_one": string; + "desc_other": string; + "tabEmail": string; + "emailPlaceholder": string; + "tabLink": string; + "linkPlaceholder": string; + "emailSend": string; + "linkSend": string; + "spaceTitle": string; + "collaboratorSearchPlaceholder": string; + "collaboratorJoin": string; + "collaboratorRemove": string; + "linkTitle": string; + "linkCreatedTime": string; + "linkCopySuccess": string; + "linkRemove": string; + "desc_billable_one": string; + "desc_billable_other": string; + "baseTitle": string; + "allCollaboratorsTitle": string; + "baseOnly": string; + "desc": string; + }; + "base": { + "title": string; + "desc_one": string; + "desc_other": string; + "baseTitle": string; + "collaboratorSearchPlaceholder": string; + }; + "addOrgCollaborator": { + "title": string; + "placeholder": string; + }; + }; + "help": { + "title": string; + "apiLink": string; + "appLink": string; + "mainLink": string; + }; + "pagePermissionChangeTip": string; + "listEmptyTips": string; + "billing": { + "overLimits": string; + "overLimitsDescription": string; + "userLimitExceededDescription": string; + "unavailableInPlanTips": string; + "unavailableConnectionTips": string; + "levelTips": string; + "licenseExpiredGracePeriod": string; + "spaceSubscriptionModal": { + "title": string; + "description": string; + }; + "status": { + "active": string; + "canceled": string; + "incomplete": string; + "incompleteExpired": string; + "trialing": string; + "pastDue": string; + "unpaid": string; + "paused": string; + "seatLimitExceeded": string; + }; + "enterpriseFeature": string; + "automationRequiresUpgrade": string; + "authorityMatrixRequiresUpgrade": string; + "viewPricing": string; + "billable": string; + }; + "admin": { + "setting": { + "description": string; + "allowSignUp": string; + "allowSignUpDescription": string; + "allowSpaceInvitation": string; + "allowSpaceInvitationDescription": string; + "allowSpaceCreation": string; + "allowSpaceCreationDescription": string; + "enableEmailVerification": string; + "enableEmailVerificationDescription": string; + "enableWaitlist": string; + "enableWaitlistDescription": string; + "generalSettings": string; + "aiSettings": string; + "brandingSettings": { + "title": string; + "description": string; + "logo": string; + "logoDescription": string; + "logoUpload": string; + "logoUploadDescription": string; + "brandName": string; + }; + "ai": { + "name": string; + "nameDescription": string; + "enable": string; + "enableDescription": string; + "updateLLMProvider": string; + "addProvider": string; + "addProviderDescription": string; + "providerType": string; + "baseUrl": string; + "apiKey": string; + "baseUrlDescription": string; + "apiKeyDescription": string; + "models": string; + "modelsDescription": string; + "baseUrlRequired": string; + "fetchModelListError": string; + "provider": string; + "providerDescription": string; + "modelPreferences": string; + "modelPreferencesDescription": string; + "embeddingModel": string; + "embeddingModelDescription": string; + "translationModel": string; + "translationModelDescription": string; + "chatModel": string; + "chatModelDescription": string; + "chatModels": { + "sm": string; + "smDescription": string; + "md": string; + "mdDescription": string; + "lg": string; + "lgDescription": string; + }; + "chatModelTest": { + "text": string; + "description": string; + "notConfigLgModel": string; + "confirmTitle": string; + "confirmDescription": string; + "confirm": string; + "cancel": string; + "missingCapabilitiesWarning": string; + "enableAITitle": string; + "enableAIDescription": string; + "enableAI": string; + "skipTest": string; + }; + "chatModelAbility": { + "image": string; + "pdf": string; + "webSearch": string; + "disabledWebSearch": string; + "lgModelAbility": string; + }; + "configUpdated": string; + "noModelFound": string; + "searchModel": string; + "selectModel": string; + "input": string; + "output": string; + "inputOrOutputTip": string; + "imageOutput": string; + "imageOutputTip": string; + "supportImageOutputTip": string; + "supportVisionTip": string; + "supportAudioTip": string; + "supportVideoTip": string; + "supportDeepThinkTip": string; + "testConnection": string; + "testing": string; + "testSuccess": string; + "testFailed": string; + "fillRequiredFields": string; + "modelsRequired": string; + "noValidModel": string; + "addCustomModel": string; + "isOpenRouter": string; + "customModel": string; + "customModelDescription": string; + "aiAbilitySettings": string; + "aiAbilitySettingsDescription": string; + "actions": { + "aiBasicCapability": { + "title": string; + "description": string; + }; + "buildBase": { + "title": string; + "description": string; + }; + "buildApp": { + "title": string; + "description": string; + }; + "buildAutomation": { + "title": string; + "description": string; + }; + "baseResource": { + "title": string; + "description": string; + }; + "suggestion": { + "title": string; + "description": string; + }; + }; + }; + "webSearch": { + "description": string; + }; + "instanceTitle": string; + }; + "action": { + "enterApiKey": string; + "goToConfiguration": string; + }; + "tips": { + "thankYouForUsingTeable": string; + "pleaseGoToConfiguration": string; + "pleaseContactAdmin": string; + }; + "configuration": { + "title": string; + "description": string; + "copyInstance": string; + "list": { + "publicOrigin": { + "title": string; + "description": string; + }; + "https": { + "title": string; + "description": string; + }; + "databaseProxy": { + "title": string; + "description": string; + "href": string; + }; + "llmApi": { + "title": string; + "description": string; + "errorTips": string; + }; + "app": { + "title": string; + "description": string; + "errorTips": string; + }; + "webSearch": { + "title": string; + "description": string; + "errorTips": string; + }; + "email": { + "title": string; + "description": string; + "errorTips": string; + }; + }; + }; + }; + "notification": { + "title": string; + "unread": string; + "read": string; + "markAs": string; + "markAllAsRead": string; + "noUnread": string; + "changeSetting": string; + "new": string; + "showMore": string; + "exportBase": { + "successText": string; + }; + }; + "role": { + "title": { + "owner": string; + "creator": string; + "editor": string; + "commenter": string; + "viewer": string; + }; + "description": { + "owner": string; + "creator": string; + "editor": string; + "commenter": string; + "viewer": string; + }; + }; + "trash": { + "type": string; + "resetTrash": string; + "deletedBy": string; + "deletedTime": string; + "fromSpace": string; + "permanentDeleteTips": string; + "resetTrashConfirm": string; + "addToTrash": string; + "description": string; + }; + "pluginCenter": { + "pluginUrlEmpty": string; + "install": string; + "publisher": string; + "lastUpdated": string; + "pluginNotFound": string; + "pluginEmpty": { + "title": string; + }; + }; + "automation": { + "turnOnTip": string; + }; + "email": { + "send": string; + "config": string; + "customConfig": string; + "notify": string; + "automation": string; + "customNotifyConfig": string; + "customAutomationConfig": string; + "addConfig": string; + "editConfig": string; + "resetConfig": string; + "testEmail": string; + "testEmailPlaceholder": string; + "testEmailError": string; + "testEmailSend": string; + "configError": string; + "host": string; + "hostDescription": string; + "port": string; + "secure": string; + "auth": string; + "username": string; + "password": string; + "sender": string; + "senderName": string; + "subscribe": string; + "unsubscribe": string; + "unsubscribeList": string; + "unsubscribeTime": string; + "processing": string; + "unsubscribeH1": string; + "unsubscribeH2": string; + "subscribeH1": string; + "subscribeH2": string; + "unsubscribeListTip": string; + "templates": { + "resetPassword": { + "subject": string; + "title": string; + "message": string; + "buttonText": string; + }; + "emailVerifyCode": { + "signupVerification": { + "subject": string; + "title": string; + "message": string; + }; + "domainVerification": { + "subject": string; + "title": string; + "message": string; + }; + "changeEmailVerification": { + "subject": string; + "title": string; + "message": string; + }; + }; + "collaboratorCellTag": { + "subject": string; + "title": string; + "buttonText": string; + }; + "collaboratorMultiRowTag": { + "subject": string; + "title": string; + "buttonText": string; + }; + "invite": { + "subject": string; + "title": string; + "message": string; + "buttonText": string; + }; + "waitlistInvite": { + "subject": string; + "title": string; + "message": string; + "buttonText": string; + }; + "test": { + "subject": string; + "title": string; + "message": string; + }; + "notify": { + "subject": string; + "title": string; + "buttonText": string; + "import": { + "title": string; + "table": { + "aborted": { + "message": string; + }; + "failed": { + "message": string; + }; + "success": { + "message": string; + "inplace": string; + }; + }; + }; + "recordComment": { + "title": string; + "message": string; + }; + "automation": { + "title": string; + "failed": { + "title": string; + "message": string; + }; + "insufficientCredit": { + "title": string; + "message": string; + }; + }; + "exportBase": { + "title": string; + "success": { + "message": string; + }; + "failed": { + "message": string; + }; + }; + "task": { + "ai": { + "failed": { + "title": string; + "message": string; + }; + }; + }; + }; + }; + "title": string; + }; + "waitlist": { + "title": string; + "email": string; + "joinTitle": string; + "joinDesc": string; + "emailPlaceholder": string; + "youAreOnTheList": string; + "thanksForJoining": string; + "back": string; + "inviteCodePlaceholder": string; + "join": string; + "joining": string; + "invite": string; + "inviteTime": string; + "createdTime": string; + "yes": string; + "no": string; + "generateCode": string; + "count": string; + "times": string; + "generate": string; + "code": string; + "inviteSuccess": string; + "app": { + "previewAppError": string; + "sendErrorToAI": string; + }; + }; + "base": { + "deleteTip": string; + }; + "noDescription": string; + "noPermissionToCreateBase": string; + "app": { + "title": string; + "description": string; + "previewAppError": string; + "sendErrorToAI": string; + }; + }; + "dashboard": { + "empty": { + "title": string; + "description": string; + "create": string; + }; + "addPlugin": string; + "createDashboard": { + "button": string; + "title": string; + "placeholder": string; + }; + "findDashboard": string; + "expand": string; + "pluginUrlEmpty": string; + "install": string; + "publisher": string; + "lastUpdated": string; + "pluginNotFound": string; + "pluginEmpty": { + "title": string; + }; + }; + "developer": { + "apiQueryBuilder": string; + "subTitle": string; + "apiList": string; + "cellFormat": string; + "fieldKeyType": string; + "fieldKeyTypeDesc": string; + "chooseSource": string; + "action": { + "selectBase": string; + "selectTable": string; + }; + "pickParams": string; + "buildResult": string; + "buildResultEmpty": string; + "previewReturnValue": string; + "replaceToken": string; + "createNewToken": string; + "showPagination": string; + "addSort": string; + "only10Records": string; + }; + "oauth": { + "add": string; + "title": { + "add": string; + "edit": string; + "description": string; + }; + "form": { + "name": { + "label": string; + "description": string; + }; + "description": { + "label": string; + "description": string; + }; + "homePageUrl": { + "label": string; + "description": string; + }; + "logo": { + "label": string; + "description": string; + "placeholder": string; + "button": string; + "clear": string; + "lengthError": string; + "typeError": string; + "Label": string; + }; + "callbackUrl": { + "label": string; + "description": string; + "add": string; + }; + "scopes": { + "label": string; + "description": string; + }; + "secret": { + "label": string; + "add": string; + "newDescription": string; + "empty": string; + "lastUsed": string; + "tag": string; + "neverUsed": string; + }; + "clientId": { + "label": string; + }; + }; + "formType": { + "basic": string; + "scopes": string; + "identify": string; + "clientInfo": string; + }; + "decision": { + "title": string; + "scopes": string; + "redirectDescription": string; + "authorize": string; + }; + "help": { + "link": string; + "title": string; + }; + "deleteConfirm": { + "title": string; + "description": string; + }; + }; + "plugin": { + "add": string; + "title": { + "add": string; + "edit": string; + }; + "pluginUser": { + "name": string; + "description": string; + }; + "secret": string; + "regenerateSecret": string; + "form": { + "name": { + "label": string; + "description": string; + }; + "description": { + "label": string; + "description": string; + }; + "detailDesc": { + "label": string; + "description": string; + }; + "logo": { + "label": string; + "description": string; + "upload": string; + "clear": string; + "placeholder": string; + "lengthError": string; + "typeError": string; + "Label": string; + }; + "helpUrl": { + "label": string; + "description": string; + }; + "positions": { + "label": string; + "description": string; + }; + "i18n": { + "label": string; + "description": string; + }; + "url": { + "label": string; + "description": string; + }; + "autoCreateMember": { + "label": string; + "description": string; + }; + "config": { + "label": string; + "description": string; + }; + }; + "markdown": { + "write": string; + "preview": string; + }; + "status": { + "reviewing": string; + "published": string; + "developing": string; + }; + "button": { + "submitApproved": string; + }; + }; + "sdk": { + "common": { + "comingSoon": string; + "empty": string; + "noRecords": string; + "unnamedRecord": string; + "untitled": string; + "cancel": string; + "confirm": string; + "back": string; + "done": string; + "create": string; + "search": { + "placeholder": string; + "empty": string; + }; + "readOnlyTip": string; + "selectPlaceHolder": string; + "loading": string; + "loadMore": string; + "uploadFailed": string; + "rowCount": string; + "summary": string; + "summaryTip": string; + "actions": string; + "remove": string; + "runStatus": { + "success": string; + "failed": string; + "running": string; + }; + "resetSuccess": string; + "click": string; + "clickedCount": string; + }; + "notification": { + "title": string; + }; + "preview": { + "previewFileLimit": string; + "loadFileError": string; + }; + "undoRedo": { + "undo": string; + "redo": string; + "undoFailed": string; + "redoFailed": string; + "nothingToUndo": string; + "nothingToRedo": string; + "undoSucceed": string; + "redoSucceed": string; + "undoing": string; + "redoing": string; + }; + "editor": { + "attachment": { + "uploadDragOver": string; + "uploadDragDefault": string; + "upload": string; + "uploadBaseTextPrefix": string; + "uploadBaseText": string; + }; + "date": { + "placeholder": string; + "today": string; + }; + "formula": { + "title": string; + "guideSyntax": string; + "guideExample": string; + "helperExample": string; + "fieldValue": string; + "placeholder": string; + "placeholderForAIPrompt": string; + "editExpression": string; + "generateExpressionByAI": string; + "inputPrompt": string; + "generateExpression": string; + "generatingByAI": string; + "generatedExpressionTips": string; + "action": { + "generating": string; + "generate": string; + "apply": string; + }; + "expressionRequired": string; + }; + "link": { + "placeholder": string; + "searchPlaceholder": string; + "create": string; + "selectRecord": string; + "unselected": string; + "selected": string; + "expandRecordError": string; + "alreadyOpen": string; + "goToForeignTable": string; + "foreignTableIdRequired": string; + "linkFieldIdRequired": string; + "selectTooManyRecords": string; + "relationshipRequired": string; + }; + "user": { + "searchPlaceholder": string; + "notify": string; + }; + "select": { + "addOption": string; + "choicesNameRequired": string; + }; + "lookup": { + "lookupFieldIdRequired": string; + "lookupOptionsNotAllowed": string; + "lookupOptionsRequired": string; + "refineOptionsError": string; + }; + "rollup": { + "expressionRequired": string; + "unsupportedTip": string; + }; + "conditionalRollup": { + "filterRequired": string; + }; + "conditionalLookup": { + "filterRequired": string; + }; + "aiConfig": { + "modelKeyRequired": string; + "typeNotSupported": string; + "sourceFieldIdRequired": string; + "targetLanguageRequired": string; + "promptRequired": string; + }; + "error": { + "refineOptionsError": string; + "optionsRequired": string; + }; + }; + "filter": { + "label": string; + "displayLabel": string; + "displayLabel_other": string; + "addCondition": string; + "addConditionGroup": string; + "nestedLimitTip": string; + "linkInputPlaceholder": string; + "groupDescription": string; + "currentUser": string; + "tips": { + "scope": string; + }; + "invalidateSelected": string; + "invalidateSelectedTips": string; + "default": { + "empty": string; + "placeholder": string; + }; + "conjunction": { + "and": string; + "or": string; + "where": string; + }; + "operator": { + "is": string; + "isNot": string; + "contains": string; + "doesNotContain": string; + "isEmpty": string; + "isNotEmpty": string; + "isGreater": string; + "isGreaterEqual": string; + "isLess": string; + "isLessEqual": string; + "isAnyOf": string; + "isNoneOf": string; + "hasAnyOf": string; + "hasAllOf": string; + "hasNoneOf": string; + "isExactly": string; + "isWithIn": string; + "isBefore": string; + "isAfter": string; + "isOnOrBefore": string; + "isOnOrAfter": string; + "number": { + "is": string; + "isNot": string; + "isGreater": string; + "isGreaterEqual": string; + "isLess": string; + "isLessEqual": string; + }; + }; + "component": { + "date": { + "today": string; + "tomorrow": string; + "yesterday": string; + "oneWeekAgo": string; + "oneWeekFromNow": string; + "oneMonthAgo": string; + "oneMonthFromNow": string; + "daysAgo": string; + "daysFromNow": string; + "exactDate": string; + "exactFormatDate": string; + "currentWeek": string; + "currentMonth": string; + "currentYear": string; + "lastWeek": string; + "lastMonth": string; + "lastYear": string; + "nextWeekPeriod": string; + "nextMonthPeriod": string; + "nextYearPeriod": string; + "pastWeek": string; + "pastMonth": string; + "pastYear": string; + "nextWeek": string; + "nextMonth": string; + "nextYear": string; + "pastNumberOfDays": string; + "nextNumberOfDays": string; + }; + }; + "conditionalRollup": { + "switchToField": string; + "switchToValue": string; + }; + }; + "color": { + "label": string; + }; + "rowHeight": { + "short": string; + "medium": string; + "tall": string; + "extraTall": string; + "title": string; + }; + "fieldNameConfig": { + "title": string; + "displayLines": string; + }; + "share": { + "title": string; + }; + "extensions": { + "title": string; + }; + "hidden": { + "label": string; + "configLabel_one": string; + "configLabel_other": string; + "configLabel_other_visible": string; + "showAll": string; + "hideAll": string; + "primaryKey": string; + }; + "expandRecord": { + "copy": string; + "duplicateRecord": string; + "copyRecordUrl": string; + "deleteRecord": string; + "addRecordComment": string; + "viewRecordHistory": string; + "recordHistory": { + "hiddenRecordHistory": string; + "showRecordHistory": string; + "createdTime": string; + "createdBy": string; + "before": string; + "after": string; + "viewRecord": string; + }; + "showHiddenFields": string; + "hideHiddenFields": string; + "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438": string; + }; + "sort": { + "label": string; + "displayLabel_one": string; + "displayLabel_other": string; + "setTips": string; + "addButton": string; + "autoSort": string; + "selectASCLabel": string; + "selectDESCLabel": string; + }; + "group": { + "label": string; + "displayLabel_one": string; + "displayLabel_other": string; + "setTips": string; + "addButton": string; + }; + "field": { + "title": { + "singleLineText": string; + "longText": string; + "singleSelect": string; + "number": string; + "multipleSelect": string; + "link": string; + "formula": string; + "date": string; + "createdTime": string; + "lastModifiedTime": string; + "attachment": string; + "checkbox": string; + "rollup": string; + "conditionalRollup": string; + "user": string; + "rating": string; + "autoNumber": string; + "lookup": string; + "conditionalLookup": string; + "button": string; + "createdBy": string; + "lastModifiedBy": string; + }; + "description": { + "singleLineText": string; + "longText": string; + "singleSelect": string; + "number": string; + "multipleSelect": string; + "link": string; + "formula": string; + "date": string; + "createdTime": string; + "lastModifiedTime": string; + "attachment": string; + "checkbox": string; + "rollup": string; + "conditionalRollup": string; + "user": string; + "rating": string; + "autoNumber": string; + "lookup": string; + "conditionalLookup": string; + "button": string; + "createdBy": string; + "lastModifiedBy": string; + }; + "link": { + "oneWay": string; + "twoWay": string; + }; + }; + "permission": { + "actionDescription": { + "spaceCreate": string; + "spaceDelete": string; + "spaceRead": string; + "spaceUpdate": string; + "spaceInviteEmail": string; + "spaceInviteLink": string; + "spaceGrantRole": string; + "baseCreate": string; + "baseDelete": string; + "baseRead": string; + "baseReadAll": string; + "baseUpdate": string; + "baseInviteEmail": string; + "baseInviteLink": string; + "baseTableImport": string; + "baseAuthorityMatrixConfig": string; + "baseDbConnect": string; + "tableCreate": string; + "tableRead": string; + "tableDelete": string; + "tableUpdate": string; + "tableImport": string; + "tableExport": string; + "tableTrashRead": string; + "tableTrashUpdate": string; + "tableTrashReset": string; + "viewCreate": string; + "viewDelete": string; + "viewRead": string; + "viewUpdate": string; + "viewShare": string; + "fieldCreate": string; + "fieldDelete": string; + "fieldRead": string; + "fieldUpdate": string; + "recordCreate": string; + "recordComment": string; + "recordDelete": string; + "recordRead": string; + "recordUpdate": string; + "automationCreate": string; + "automationDelete": string; + "automationRead": string; + "automationUpdate": string; + "userProfileRead": string; + "userEmailRead": string; + "recordHistoryRead": string; + "baseQuery": string; + "instanceRead": string; + "instanceUpdate": string; + "enterpriseRead": string; + "enterpriseUpdate": string; + "recordCopy": string; + }; + }; + "noun": { + "table": string; + "view": string; + "space": string; + "base": string; + "field": string; + "record": string; + "automation": string; + "user": string; + "recordHistory": string; + "you": string; + "instance": string; + "enterprise": string; + }; + "formula": { + "SUM": { + "summary": string; + "example": string; + }; + "AVERAGE": { + "summary": string; + "example": string; + }; + "MAX": { + "summary": string; + "example": string; + }; + "MIN": { + "summary": string; + "example": string; + }; + "ROUND": { + "summary": string; + "example": string; + }; + "ROUNDUP": { + "summary": string; + "example": string; + }; + "ROUNDDOWN": { + "summary": string; + "example": string; + }; + "CEILING": { + "summary": string; + "example": string; + }; + "FLOOR": { + "summary": string; + "example": string; + }; + "EVEN": { + "summary": string; + "example": string; + }; + "ODD": { + "summary": string; + "example": string; + }; + "INT": { + "summary": string; + "example": string; + }; + "ABS": { + "summary": string; + "example": string; + }; + "SQRT": { + "summary": string; + "example": string; + }; + "POWER": { + "summary": string; + "example": string; + }; + "EXP": { + "summary": string; + "example": string; + }; + "LOG": { + "summary": string; + "example": string; + }; + "MOD": { + "summary": string; + "example": string; + }; + "VALUE": { + "summary": string; + "example": string; + }; + "CONCATENATE": { + "summary": string; + "example": string; + }; + "FIND": { + "summary": string; + "example": string; + }; + "SEARCH": { + "summary": string; + "example": string; + }; + "MID": { + "summary": string; + "example": string; + }; + "LEFT": { + "summary": string; + "example": string; + }; + "RIGHT": { + "summary": string; + "example": string; + }; + "REPLACE": { + "summary": string; + "example": string; + }; + "REGEXP_REPLACE": { + "summary": string; + "example": string; + }; + "SUBSTITUTE": { + "summary": string; + "example": string; + }; + "LOWER": { + "summary": string; + "example": string; + }; + "UPPER": { + "summary": string; + "example": string; + }; + "REPT": { + "summary": string; + "example": string; + }; + "TRIM": { + "summary": string; + "example": string; + }; + "LEN": { + "summary": string; + "example": string; + }; + "T": { + "summary": string; + "example": string; + }; + "ENCODE_URL_COMPONENT": { + "summary": string; + "example": string; + }; + "IF": { + "summary": string; + "example": string; + }; + "SWITCH": { + "summary": string; + "example": string; + }; + "AND": { + "summary": string; + "example": string; + }; + "OR": { + "summary": string; + "example": string; + }; + "XOR": { + "summary": string; + "example": string; + }; + "NOT": { + "summary": string; + "example": string; + }; + "BLANK": { + "summary": string; + "example": string; + }; + "ERROR": { + "summary": string; + "example": string; + }; + "IS_ERROR": { + "summary": string; + "example": string; + }; + "TODAY": { + "summary": string; + "example": string; + }; + "NOW": { + "summary": string; + "example": string; + }; + "YEAR": { + "summary": string; + "example": string; + }; + "MONTH": { + "summary": string; + "example": string; + }; + "WEEKNUM": { + "summary": string; + "example": string; + }; + "WEEKDAY": { + "summary": string; + "example": string; + }; + "DAY": { + "summary": string; + "example": string; + }; + "HOUR": { + "summary": string; + "example": string; + }; + "MINUTE": { + "summary": string; + "example": string; + }; + "SECOND": { + "summary": string; + "example": string; + }; + "FROMNOW": { + "summary": string; + "example": string; + }; + "TONOW": { + "summary": string; + "example": string; + }; + "DATETIME_DIFF": { + "summary": string; + "example": string; + }; + "WORKDAY": { + "summary": string; + "example": string; + }; + "WORKDAY_DIFF": { + "summary": string; + "example": string; + }; + "IS_SAME": { + "summary": string; + "example": string; + }; + "IS_AFTER": { + "summary": string; + "example": string; + }; + "IS_BEFORE": { + "summary": string; + "example": string; + }; + "DATE_ADD": { + "summary": string; + "example": string; + }; + "DATESTR": { + "summary": string; + "example": string; + }; + "TIMESTR": { + "summary": string; + "example": string; + }; + "DATETIME_FORMAT": { + "summary": string; + "example": string; + }; + "DATETIME_PARSE": { + "summary": string; + "example": string; + }; + "CREATED_TIME": { + "summary": string; + "example": string; + }; + "LAST_MODIFIED_TIME": { + "summary": string; + "example": string; + }; + "COUNTALL": { + "summary": string; + "example": string; + }; + "COUNTA": { + "summary": string; + "example": string; + }; + "COUNT": { + "summary": string; + "example": string; + }; + "ARRAY_JOIN": { + "summary": string; + "example": string; + }; + "ARRAY_UNIQUE": { + "summary": string; + "example": string; + }; + "ARRAY_FLATTEN": { + "summary": string; + "example": string; + }; + "ARRAY_COMPACT": { + "summary": string; + "example": string; + }; + "TEXT_ALL": { + "summary": string; + "example": string; + }; + "RECORD_ID": { + "summary": string; + "example": string; + }; + "AUTO_NUMBER": { + "summary": string; + "example": string; + }; + "FORMULA": { + "summary": string; + "example": string; + }; + }; + "functionType": { + "fields": string; + "numeric": string; + "text": string; + "logical": string; + "date": string; + "array": string; + "system": string; + }; + "statisticFunc": { + "none": string; + "count": string; + "empty": string; + "filled": string; + "unique": string; + "max": string; + "min": string; + "sum": string; + "average": string; + "checked": string; + "unChecked": string; + "percentEmpty": string; + "percentFilled": string; + "percentUnique": string; + "percentChecked": string; + "percentUnChecked": string; + "earliestDate": string; + "latestDate": string; + "dateRangeOfDays": string; + "dateRangeOfMonths": string; + "totalAttachmentSize": string; + "%percentChecked": string; + }; + "baseQuery": { + "add": string; + "error": { + "invalidCol": string; + "invalidCols": string; + "invalidTable": string; + "requiredSelect": string; + }; + "from": { + "title": string; + "fromTable": string; + "fromQuery": string; + }; + "select": { + "title": string; + }; + "where": { + "title": string; + }; + "groupBy": { + "title": string; + }; + "orderBy": { + "title": string; + "asc": string; + "desc": string; + }; + "limit": { + "title": string; + }; + "offset": { + "title": string; + }; + "join": { + "title": string; + "joinType": string; + "leftJoin": string; + "rightJoin": string; + "innerJoin": string; + "fullJoin": string; + "data": string; + }; + "aggregation": { + "title": string; + }; + }; + "comment": { + "title": string; + "placeholder": string; + "emptyComment": string; + "deletedComment": string; + "imageSizeLimit": string; + "tip": { + "editing": string; + "edited": string; + "notifyAll": string; + "notifyRelatedToMe": string; + "all": string; + "relatedToMe": string; + "reactionUserSuffix": string; + "me": string; + "connection": string; + }; + "toolbar": { + "link": string; + "image": string; + }; + "floatToolbar": { + "editLink": string; + "caption": string; + "delete": string; + "linkText": string; + "enterUrl": string; + }; + }; + "memberSelector": { + "title": string; + "memberSelectorSearchPlaceholder": string; + "departmentSelectorSearchPlaceholder": string; + "selected": string; + "noSelected": string; + "empty": string; + "emptyDepartment": string; + }; + "httpErrors": { + "validationError": string; + "invalidCaptcha": string; + "invalidCredentials": string; + "unauthorized": string; + "unauthorizedShare": string; + "paymentRequired": string; + "restrictedResource": string; + "notFound": string; + "conflict": string; + "unprocessableEntity": string; + "userLimitExceeded": string; + "tooManyRequests": string; + "internalServerError": string; + "databaseConnectionUnavailable": string; + "gatewayTimeout": string; + "unknownErrorCode": string; + "viewNotFound": string; + "requestTimeout": string; + "failedDependency": string; + "automationNodeParseError": string; + "automationNodeNeedTest": string; + "automationNodeTestOutdated": string; + "custom": { + "fieldValueNotNull": string; + "fieldValueDuplicate": string; + "linkFieldValueDuplicate": string; + "requestTimeout": string; + "searchTimeOut": string; + "dependencyNodeRequire": string; + }; + "email": { + "testEmailError": string; + }; + "automation": { + "buttonClickTriggerDuplicated": string; + "triggerNotFound": string; + "nodeNotFound": string; + "workflowNotFound": string; + "triggerTestFailed": string; + "workflowTestFailed": string; + "nodeParseError": string; + "nodeNeedTest": string; + "nodeTestOutdated": string; + }; + "permission": { + "createRecordWithDeniedFields": string; + "deleteRecords": string; + "readRecordWithDeniedFields": string; + "updateRecordWithDeniedFields": string; + }; + "field": { + "unsupportedFieldType": string; + "unsupportedPrimaryFieldType": string; + "calculateRecordNotFound": string; + "toRecordIdsOrFromRecordIdsRequired": string; + "recordFieldsRequired": string; + "uniqueUnsupportedType": string; + "notNullValidationWhenCreateField": string; + "dbFieldNameAlreadyExists": string; + "fieldValidationError": string; + "fieldNameAlreadyExists": string; + "fieldNotFound": string; + "fieldNotFoundInTable": string; + "deleteFieldsNotFound": string; + "lookupValuesShouldBeArray": string; + "linkCellValuesShouldBeArray": string; + "lookupAndLinkLengthMatch": string; + "cycleDetected": string; + "cycleDetectedCreateField": string; + "recordMapNotFound": string; + "forbidDeletePrimaryField": string; + "foreignTableIdInvalid": string; + "relationshipInvalid": string; + "linkFieldIdInvalid": string; + "lookupFieldIdInvalid": string; + "formulaExpressionParseError": string; + "formulaReferenceNotFound": string; + "rollupExpressionParseError": string; + "choiceNameAlreadyExists": string; + "symmetricFieldIdRequired": string; + "foreignKeyNameCannotUseId": string; + "createForeignKeyError": string; + "lookupFieldTypeNotEqual": string; + "recordNotFound": string; + "linkCellRecordIdAlreadyExists": string; + "linkConsistencyError": string; + "oneOneLinkCellValueCannotBeArray": string; + "manyOneLinkCellValueCannotBeArray": string; + "oneManyLinkCellValueShouldBeArray": string; + "manyManyLinkCellValueShouldBeArray": string; + "foreignKeyDuplicate": string; + }; + "view": { + "viewNotFound": string; + }; + "billing": { + "insufficientCredit": string; + }; + }; + "spaceRole": { + "role": { + "owner": string; + "creator": string; + "editor": string; + "commenter": string; + "viewer": string; + }; + "description": { + "owner": string; + "creator": string; + "editor": string; + "commenter": string; + "viewer": string; + }; + }; + }; + "setting": { + "personalAccessToken": string; + "oauthApps": string; + "plugins": string; + }; + "share": { + "auth": { + "title": string; + "submit": string; + "password": string; + }; + "toolbar": { + "filterLinkSelectPlaceholder": string; + }; + "openOnNewPage": string; + "errorTips": string; + }; + "space": { + "initialSpaceName": string; + "action": { + "createBase": string; + "createSpace": string; + "invite": string; + }; + "allSpaces": string; + "spaceIsEmpty": string; + "baseModal": { + "copy": string; + "duplicate": string; + "createBaseFromTemplate": string; + "duplicateRecords": string; + "duplicateRecordsTip": string; + "toSpace": string; + "copyToSpace": string; + "duplicateBase": string; + "missTargetTip": string; + "copying": string; + "copyingTemplate": string; + "howToCreate": string; + "fromScratch": string; + "fromTemplate": string; + "moveBaseToAnotherSpace": string; + "chooseSpace": string; + "duplicateBaseSucceedAndJump": string; + }; + "spaceSetting": { + "title": string; + "general": string; + "collaborators": string; + "generalDescription": string; + "collaboratorDescription": string; + "spaceName": string; + "spaceId": string; + "importBase": string; + }; + "pin": { + "add": string; + "remove": string; + "pin": string; + "empty": string; + }; + "tip": { + "delete": string; + "title": string; + "exportTips1": string; + "exportTips2": string; + "exportTips3": string; + "exportIncludeDataLabel": string; + "exportIncludeDataDescription": string; + "moveBaseSuccessTitle": string; + "moveBaseSuccessDescription": string; + }; + "deleteSpaceModal": { + "title": string; + "blockedTitle": string; + "blockedDesc": string; + "permanentDeleteWarning": string; + "confirmInputLabel": string; + }; + "sharedBase": { + "title": string; + "description": string; + "empty": string; + }; + "integration": { + "title": string; + "description": string; + "addIntegration": string; + "ai": string; + }; + "collaborators": string; + "more": string; + "aiSetting": { + "title": string; + "description": string; + "enableTips": string; + "enable": string; + "enableSwitchTips": string; + }; + "import": { + "importing": string; + "importWayTip": string; + "baseImportTips": string; + "confirm": string; + }; + "template": { + "title": string; + "description": string; + }; + "recentlyBase": { + "title": string; + }; + "noBases": { + "title": string; + "description": string; + }; + "noSpaces": { + "title": string; + "description": string; + }; + }; + "system": { + "notFound": { + "title": string; + }; + "links": { + "backToHome": string; + }; + "forbidden": { + "title": string; + "description": string; + }; + "paymentRequired": { + "title": string; + "description": string; + }; + }; + "table": { + "toolbar": { + "comingSoon": string; + "viewFilterInShare": string; + "createFieldButtonText": string; + "others": { + "share": { + "label": string; + "statusLabel": string; + "noPermission": string; + "shareLink": string; + "copied": string; + "genLink": string; + "allowCopy": string; + "showAllFields": string; + "restrict": string; + "tips": string; + "passwordTitle": string; + "passwordTips": string; + "embed": string; + "embedPreview": string; + "hideToolbar": string; + "URLSetting": string; + "URLSettingDescription": string; + "cancel": string; + "save": string; + "requireLogin": string; + }; + "extensions": { + "label": string; + "graph": string; + }; + "api": { + "label": string; + "restfulApi": string; + "databaseConnection": string; + }; + "personalView": { + "personal": string; + "collaborative": string; + "dialog": { + "title": string; + "description": string; + "cancelText": string; + "confirmText": string; + }; + }; + }; + }; + "welcome": { + "title": string; + "description": string; + "help": string; + "helpCenter": string; + }; + "field": { + "fieldManagement": string; + "fieldManagementDesc": string; + "advancedProps": string; + "hide": string; + "default": { + "singleLineText": { + "title": string; + }; + "longText": { + "title": string; + }; + "number": { + "title": string; + "formatType": string; + "currencySymbol": string; + "defaultSymbol": string; + "precision": string; + "decimalExample": string; + "currencyExample": string; + "percentExample": string; + "CurrencySymbol": string; + "%Example": string; + }; + "singleSelect": { + "title": string; + "options": { + "todo": string; + "inProgress": string; + "done": string; + }; + }; + "multipleSelect": { + "title": string; + }; + "attachment": { + "title": string; + }; + "user": { + "title": string; + }; + "date": { + "title": string; + "dateFormatting": string; + "timeFormatting": string; + "timeZone": string; + "yearMonth": string; + "monthDay": string; + "year": string; + "month": string; + "day": string; + "local": string; + "friendly": string; + "us": string; + "european": string; + "asia": string; + "custom": string; + "12Hour": string; + "24Hour": string; + "noDisplay": string; + }; + "autoNumber": { + "title": string; + }; + "createdTime": { + "title": string; + }; + "lastModifiedTime": { + "title": string; + }; + "createdBy": { + "title": string; + }; + "lastModifiedBy": { + "title": string; + }; + "rating": { + "title": string; + }; + "checkbox": { + "title": string; + }; + "button": { + "title": string; + "label": string; + "color": string; + "limitCount": string; + "resetCount": string; + "maxCount": string; + "automation": string; + "customAutomation": string; + }; + "formula": { + "title": string; + "formula": string; + }; + "lookup": { + "title": string; + }; + "conditionalLookup": { + "title": string; + }; + "rollup": { + "title": string; + "rollup": string; + "selectAnRollupFunction": string; + "func": { + "and": string; + "arrayCompact": string; + "arrayJoin": string; + "arrayUnique": string; + "average": string; + "concatenate": string; + "count": string; + "countA": string; + "countAll": string; + "max": string; + "min": string; + "or": string; + "sum": string; + "xor": string; + }; + "funcDesc": { + "and": string; + "arrayCompact": string; + "arrayJoin": string; + "arrayUnique": string; + "average": string; + "concatenate": string; + "count": string; + "countA": string; + "countAll": string; + "max": string; + "min": string; + "or": string; + "sum": string; + "xor": string; + }; + }; + "conditionalRollup": { + "title": string; + "description": string; + }; + }; + "editor": { + "addField": string; + "editField": string; + "insertField": string; + "graph": string; + "defaultValue": string; + "reset": string; + "fieldUpdated": string; + "fieldCreated": string; + "previewDependenciesGraph": string; + "areYouSurePerformIt": string; + "addDescription": string; + "dbFieldName": string; + "description": string; + "descriptionPlaceholder": string; + "type": string; + "showAs": string; + "color": string; + "number": string; + "chartBar": string; + "chartLine": string; + "ring": string; + "bar": string; + "text": string; + "url": string; + "email": string; + "phone": string; + "maxNumber": string; + "showNumber": string; + "autoFillDate": string; + "createSymmetricLink": string; + "allowLinkMultipleRecords": string; + "allowLinkToDuplicateRecords": string; + "allowSymmetricFieldLinkMultipleRecords": string; + "oneToOne": string; + "oneToMany": string; + "manyToOne": string; + "manyToMany": string; + "self": string; + "selectTable": string; + "selectBase": string; + "linkFromAnotherBase": string; + "inSelfLink": string; + "betweenTwoTables": string; + "tips": string; + "linkTipMessage": string; + "style": string; + "maximum": string; + "addOption": string; + "allowMultiUsers": string; + "notifyUsers": string; + "searchTable": string; + "calculating": string; + "doSaveChanges": string; + "linkFieldToLookup": string; + "lookupToTable": string; + "rollupToTable": string; + "selectField": string; + "linkTable": string; + "linkBase": string; + "tableNoPermission": string; + "baseNoPermission": string; + "noLinkTip": string; + "fieldValidationRules": string; + "enableValidateFieldUnique": string; + "enableValidateFieldNotNull": string; + "knowMore": string; + "linkFieldKnowMoreLink": string; + "showByField": string; + "filterByView": string; + "filter": string; + "hideFields": string; + "moreOptions": string; + "allowNewOptionsWhenEditing": string; + "conditionalLookup": { + "sortLimitToggleLabel": string; + "sortLabel": string; + "orderPlaceholder": string; + "clearSort": string; + "limitLabel": string; + "limitPlaceholder": string; + "limitHint": string; + "sortMissingWarningTitle": string; + "sortMissingWarningDescription": string; + }; + "conditionalRollup": { + "fieldMapping": string; + "selectBaseField": string; + "noMappings": string; + }; + }; + "subTitle": { + "link": string; + "singleLineText": string; + "longText": string; + "attachment": string; + "checkbox": string; + "multipleSelect": string; + "singleSelect": string; + "user": string; + "date": string; + "number": string; + "duration": string; + "rating": string; + "formula": string; + "rollup": string; + "conditionalLookup": string; + "count": string; + "createdTime": string; + "lastModifiedTime": string; + "createdBy": string; + "lastModifiedBy": string; + "autoNumber": string; + "button": string; + "lookup": string; + "conditionalRollup": string; + }; + "fieldName": string; + "fieldNameOptional": string; + "fieldType": string; + "aiConfig": { + "title": string; + "type": { + "summary": string; + "translation": string; + "extraction": string; + "improvement": string; + "tag": string; + "classification": string; + "customization": string; + "imageGeneration": string; + "rating": string; + }; + "label": { + "type": string; + "model": string; + "targetLanguage": string; + "sourceField": string; + "sourceFieldForTag": string; + "sourceFieldForClassify": string; + "attachPrompt": string; + "prompt": string; + "sourceFieldForAttachment": string; + "imageSize": string; + "imageQuality": string; + "imageCount": string; + }; + "placeholder": { + "summarize": string; + "translate": string; + "extractInfo": string; + "extractDate": string; + "improveText": string; + "attachPromptForTag": string; + "attachPromptForClassify": string; + "attachPrompt": string; + "prompt": string; + "type": string; + "targetLanguage": string; + "imageSize": string; + "imageQuality": string; + "attachPromptForImageGeneration": string; + "attachPromptForRating": string; + }; + "imageQuality": { + "low": string; + "medium": string; + "high": string; + }; + "autoFill": { + "title": string; + "tip": string; + }; + "autoFillFieldDialog": { + "title": string; + "description": string; + }; + "action": { + "addAttachment": string; + }; + }; + }; + "table": { + "newTableLabel": string; + "rename": string; + "design": string; + "tableRecordHistory": string; + "deleteConfirm": string; + "dbTableName": string; + "schemaName": string; + "baseInfo": string; + "typeOfDatabase": string; + "descriptionForTable": string; + "nameForTable": string; + "deleteTip1": string; + "deleteTip2": string; + "operator": { + "createBlank": string; + }; + "actionTips": { + "copyAndPasteEnvironment": string; + "copyAndPasteBrowser": string; + "copying": string; + "copySuccessful": string; + "copyFailed": string; + "pasting": string; + "pasteSuccessful": string; + "pasteFailed": string; + "filling": string; + "fillSuccessful": string; + "fillFailed": string; + "clearing": string; + "clearSuccessful": string; + "deleteFieldConfirmTitle": string; + "deleting": string; + "deleteSuccessful": string; + "pasteFileFailed": string; + "copyError": { + "noFocus": string; + "noPermission": string; + }; + "clearConfirmTitle": string; + "clearConfirmDescription": string; + "deleteRecordConfirmTitle": string; + "deleteRecordConfirmDescription": string; + "pasteConfirmTitle": string; + "pasteConfirmDescription": string; + "expandCommonDescription": string; + "expandColDescription": string; + "expandRowDescription": string; + "paste": string; + "deleteRecord": string; + "clear": string; + "conjunction": string; + "pasing": string; + }; + "graph": { + "tableLabel": string; + "effectCells": string; + "estimatedTime": string; + "linkFieldCount": string; + }; + "integrity": { + "check": string; + "title": string; + "loading": string; + "allGood": string; + "fixIssues": string; + "type": string; + "message": string; + "errorType": { + "ForeignTableNotFound": string; + "ForeignKeyNotFound": string; + "SelfKeyNotFound": string; + "SymmetricFieldNotFound": string; + "MissingRecordReference": string; + "InvalidLinkReference": string; + "ForeignKeyHostTableNotFound": string; + "ReferenceFieldNotFound": string; + "UniqueIndexNotFound": string; + "EmptyString": string; + }; + }; + "index": { + "description": string; + "repair": string; + "repairTip": string; + "enableIndexTip": string; + "globalSearchTip_limited": string; + "globalSearchTip_infinity": string; + "autoIndexTip": string; + "enableIndex": string; + "keepAsIs": string; + "ignoreIndexError": string; + }; + "searchTips": { + "maxFieldTips_limited": string; + }; + "tableInfo": string; + "tableInfoDetail": string; + }; + "import": { + "title": { + "upload": string; + "import": string; + "localFile": string; + "linkUrl": string; + "linkUrlInputTitle": string; + "importTitle": string; + "incrementImportTitle": string; + "optionsTitle": string; + "primitiveFields": string; + "importFields": string; + "primaryField": string; + "tipsTitle": string; + "confirm": string; + }; + "menu": { + "addFromOtherSource": string; + "excelFile": string; + "csvFile": string; + "cancel": string; + "leave": string; + "downAsCsv": string; + "importData": string; + "duplicate": string; + "includeRecords": string; + "autoFill": string; + "importing": string; + }; + "tips": { + "importWayTip": string; + "leaveTip": string; + "fileExceedSizeTip": string; + "analyzing": string; + "importing": string; + "notSupportFieldType": string; + "resultEmpty": string; + "searchPlaceholder": string; + "importAlert": string; + "noTips": string; + }; + "options": { + "autoSelectFieldOptionName": string; + "useFirstRowAsHeaderOptionName": string; + "importDataOptionName": string; + "sheetKey": string; + "excludeFirstRow": string; + }; + "form": { + "defaultFieldName": string; + "error": { + "urlEmptyTip": string; + "errorFileFormat": string; + "uniqueFieldName": string; + "fieldNameEmpty": string; + "atLeastAImportField": string; + "urlValidateTip": string; + }; + "option": { + "doNotImport": string; + }; + }; + }; + "export": { + "menu": { + "exportCsv": string; + }; + }; + "grid": { + "prefillingRowTitle": string; + "prefillingRowTooltip": string; + "presortRowTitle": string; + }; + "form": { + "fieldsManagement": string; + "addAll": string; + "removeAll": string; + "hideFieldTip": string; + "unableAddFieldTip": string; + "removeFromFormTip": string; + "descriptionPlaceholder": string; + "dragToFormTip": string; + "protectedFieldTip": string; + }; + "kanban": { + "toolbar": { + "hideFieldName": string; + "customizeCards": string; + "stackedBy": string; + "chooseStackingField": string; + "chooseStackingFieldDescription": string; + "hideEmptyStack": string; + "imageSetting": string; + "fit": string; + "noImage": string; + "chooseAttachmentField": string; + }; + "stack": { + "addStack": string; + "noCards": string; + "uncategorized": string; + }; + "stackMenu": { + "collapseStack": string; + "renameStack": string; + "deleteStack": string; + }; + "cardMenu": { + "insertCardAbove": string; + "insertCardBelow": string; + "expandCard": string; + "deleteCard": string; + "duplicateCard": string; + }; + "\u043F\u0430\u043D\u0435\u043B\u044C \u0456\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u0456\u0432": { + "hideFieldName": string; + "customizeCards": string; + "stackedBy": string; + "chooseStackingField": string; + "chooseStackingFieldDescription": string; + "hideEmptyStack": string; + "imageSetting": string; + "fit": string; + "noImage": string; + "chooseAttachmentField": string; + }; + "\u0441\u0442\u0435\u043A": { + "addStack": string; + "noCards": string; + "uncategorized": string; + }; + }; + "calendar": { + "toolbar": { + "config": string; + "startDateField": string; + "endDateField": string; + "titleField": string; + "colorField": string; + "colorType": string; + "customColor": string; + "alignWithRecords": string; + "ColorField": string; + }; + "placeholder": { + "selectColorField": string; + }; + "dialog": { + "startDate": string; + "endDate": string; + "notAdd": string; + "addDateField": string; + "content": string; + }; + "moreLinkText": string; + }; + "menu": { + "insertRecordAbove": string; + "insertRecordBelow": string; + "copyCells": string; + "deleteRecord": string; + "deleteAllSelectedRecords": string; + "editField": string; + "insertFieldLeft": string; + "insertFieldRight": string; + "freezeUpField": string; + "hideField": string; + "deleteField": string; + "deleteAllSelectedFields": string; + "filterField": string; + "sortField": string; + "groupField": string; + "autoFill": string; + "groupMenuTitle": string; + "expandGroup": string; + "collapseGroup": string; + "expandAllGroups": string; + "collapseAllGroups": string; + "duplicateField": string; + }; + "connection": { + "title": string; + "description": string; + "noPermission": string; + "connectionCountTip": string; + "createFailed": string; + "helpLink": string; + }; + "view": { + "addRecord": string; + "searchView": string; + "dragToolTip": string; + "insertToolTip": string; + "action": { + "rename": string; + "duplicate": string; + "delete": string; + "lock": string; + "unlock": string; + "enable": string; + }; + "category": { + "table": string; + "form": string; + "kanban": string; + "gallery": string; + "calendar": string; + }; + "crash": { + "title": string; + "description": string; + }; + "addPluginView": string; + "search": { + "field_one": string; + "field_other": string; + }; + "locked": { + "tip": string; + }; + "noView": string; + }; + "lastModifiedTime": string; + "lastModify": string; + "pasteNewRecords": { + "title": string; + "description": string; + }; + "tableTrash": { + "title": string; + "resourceType": string; + "deletedResource": string; + }; + "aiChat": { + "tool": { + "getTableFields": string; + "getTablesMeta": string; + "sqlQuery": string; + "generateScriptAction": string; + "getScriptInput": string; + "getTeableApi": string; + "args": string; + "result": string; + "dataVisualization": string; + "updateBase": string; + "thinking": string; + "toBeConfirmed": string; + "errorMessage": string; + "confirm": string; + }; + "codeBlock": { + "hiddenLines": string; + "collapseCode": string; + "code": string; + "preview": string; + }; + "newChat": string; + "noModel": string; + "noHistory": string; + "noFoundHistory": string; + "timeGroup": { + "today": string; + "oneWeek": string; + "twoWeek": string; + "oneMonth": string; + "other": string; + }; + "context": { + "button": string; + "search": string; + "searchEmpty": string; + "emptyContext": string; + }; + "inputPlaceholder": string; + "thought": string; + "meta": { + "timeCostUnit": string; + "timeCostDescription": string; + "creditDescription": string; + "tokenDescription": string; + }; + "tools": { + "getTeableApi": string; + "readFiles": string; + "writeFile": string; + "deleteFiles": string; + "listFiles": string; + "addDependencies": string; + "checkBuildErrors": string; + "lint": string; + }; + "fallback": { + "previewLoadFailed": string; + "retry": string; + "chatAborted": string; + }; + "preview": { + "deletedTable": string; + "deletedView": string; + "deletedField": string; + "deletedRecords": string; + }; + "agentName": { + "tableOperatorAgent": string; + "viewOperatorAgent": string; + "fieldOperatorAgent": string; + "recordOperatorAgent": string; + "buildBaseAgent": string; + "buildAutomationAgent": string; + }; + "confirm": { + "toBeConfirmed": string; + "deleteWarning": string; + }; + "action": { + "createTable": string; + "updateTableName": string; + "deleteTable": string; + "createView": string; + "updateViewName": string; + "deleteView": string; + "createField": string; + "createAiField": string; + "createLinkField": string; + "createLookupField": string; + "createRollupField": string; + "createFormulaField": string; + "deleteField": string; + "updateField": string; + "createRecord": string; + "deleteRecord": string; + "updateRecord": string; + "updateBase": string; + "planTask": string; + "generateTables": string; + "generatePrimaryFields": string; + "generateFields": string; + "generateViews": string; + "generateRecords": string; + "generateAIFields": string; + "generateLinkFields": string; + "generateLookupFields": string; + "generateLinkFieldsRecords": string; + "generateRollupFields": string; + "generateFormulaFields": string; + "generateWorkflow": string; + "generateTrigger": string; + "generateScriptAction": string; + "generateSendMailAction": string; + "generateAction": string; + "initialize": string; + "rename": string; + "buildTest": string; + "developTask": string; + "generateSummary": string; + "previewEnvironment": string; + "getRelativeData": string; + "getPreviousNodeOutputVariables": string; + "getApiJson": string; + "generateScriptAndDependencies": string; + "analyzingAttachment": string; + "locateResource": string; + }; + "buildFlow": { + "progress": string; + "completed": string; + "completedDesc": string; + "stepStatus": { + "initializing": string; + "naming": string; + "planning": string; + "developing": string; + "summarizing": string; + "deploying": string; + "testing": string; + }; + "moduleStatus": { + "running": string; + "completed": string; + "error": string; + "pending": string; + }; + "toolStatus": { + "running": string; + "completed": string; + "error": string; + }; + }; + "generateScript": { + "generateSuccess": string; + }; + "buildBase": { + "title": string; + "generateSuccess": string; + "generateError": string; + }; + "buildAutomation": { + "title": string; + "generateSuccess": string; + }; + "dataVisualization": { + "error": string; + }; + "tips": { + "modelTips": string; + }; + "attachment": { + "imageNotSupported": string; + "attachmentSizeExceeded": string; + }; + "suggestions": { + "recommend": string; + "ask": string; + "analyze": string; + "build": string; + }; + }; + "plugin": { + "recent": string; + "more": string; + }; + "pluginPanel": { + "empty": { + "description": string; + }; + "createPluginPanel": { + "button": string; + "title": string; + }; + "namePlaceholder": string; + }; + "addPlugin": string; + "pluginContextMenu": { + "mangeButton": string; + "manage": string; + "noPlugin": string; + "delete": string; + "deleteDescription": string; + }; + "permission": { + "cell": { + "deniedRead": string; + "deniedUpdate": string; + }; + }; + }; + "token": { + "access": string; + "name": string; + "description": string; + "scopes": string; + "expiration": string; + "createdTime": string; + "lastUse": string; + "allSpace": string; + "formLabelTips": { + "name": string; + "description": string; + "scopes": string; + "access": string; + }; + "new": { + "headerTitle": string; + "title": string; + "description": string; + "button": string; + "success": { + "title": string; + "description": string; + }; + "expirationList": { + "days": string; + "permanent": string; + "custom": string; + "pick": string; + }; + }; + "edit": { + "title": string; + "name": string; + "scopes": string; + "selectAll": string; + "cancelSelectAll": string; + }; + "refresh": { + "title": string; + "description": string; + "button": string; + }; + "accessSelect": { + "button": string; + "empty": string; + "spaceSelectItem": string; + "inputPlaceholder": string; + "fullAccess": { + "button": string; + "description": string; + "title": string; + }; + "sharedBase": string; + }; + "moreScopes": string; + "empty": { + "list": string; + "access": string; + }; + "deleteConfirm": { + "title": string; + "description": string; + }; + "noAccessConfirm": { + "title": string; + "description": string; + }; + }; + "zod": { + "errors": { + "invalid_type": string; + "invalid_type_received_undefined": string; + "invalid_type_received_null": string; + "invalid_literal": string; + "unrecognized_keys": string; + "invalid_union": string; + "invalid_union_discriminator": string; + "invalid_enum_value": string; + "invalid_arguments": string; + "invalid_return_type": string; + "invalid_date": string; + "custom": string; + "invalid_intersection_types": string; + "not_multiple_of": string; + "not_finite": string; + "invalid_string": { + "email": string; + "url": string; + "uuid": string; + "cuid": string; + "regex": string; + "datetime": string; + "startsWith": string; + "endsWith": string; + }; + "too_small": { + "array": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "string": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "number": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "set": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "date": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + }; + "too_big": { + "array": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "string": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "number": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "set": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + "date": { + "exact": string; + "inclusive": string; + "not_inclusive": string; + }; + }; + }; + "validations": { + "email": string; + "url": string; + "uuid": string; + "cuid": string; + "regex": string; + "datetime": string; + }; + "types": { + "function": string; + "number": string; + "string": string; + "nan": string; + "integer": string; + "float": string; + "boolean": string; + "date": string; + "bigint": string; + "undefined": string; + "symbol": string; + "null": string; + "array": string; + "object": string; + "unknown": string; + "promise": string; + "void": string; + "never": string; + "map": string; + "set": string; + }; + }; +}; +/* prettier-ignore */ +export type I18nPath = Path; diff --git a/apps/nestjs-backend/src/utils/i18n.ts b/apps/nestjs-backend/src/utils/i18n.ts new file mode 100644 index 0000000000..69743a257b --- /dev/null +++ b/apps/nestjs-backend/src/utils/i18n.ts @@ -0,0 +1,30 @@ +import fs from 'fs'; +import path from 'path'; + +const localPaths = [ + process.env.I18N_LOCALES_PATH || '', + path.join(__dirname, '../../../community/packages/common-i18n/src/locales'), + path.join(__dirname, '../../../packages/common-i18n/src/locales'), + path.join(__dirname, '../../node_modules/@teable/common-i18n/src/locales'), +]; + +export const getI18nPath = () => { + console.log('backend I18n path checking', __dirname, 'localPaths', localPaths); + return localPaths.filter(Boolean).find((str) => { + const exists = fs.existsSync(str); + console.log(`backend I18n path checking exists ${exists} ${str} `); + if (exists) { + console.log('backend I18n path found', str); + } + return exists; + }); +}; + +export const getI18nTypesOutputPath = () => { + const path = process.env.I18N_TYPES_OUTPUT_PATH; + console.log('backend I18n types output path:', path); + if (!path) { + return undefined; + } + return path; +}; diff --git a/apps/nestjs-backend/test/mail.e2e-spec.ts b/apps/nestjs-backend/test/mail.e2e-spec.ts index 0601146db6..5e019ce480 100644 --- a/apps/nestjs-backend/test/mail.e2e-spec.ts +++ b/apps/nestjs-backend/test/mail.e2e-spec.ts @@ -1,6 +1,7 @@ import type { INestApplication } from '@nestjs/common'; import type { ISetSettingMailTransportConfigRo, ITestMailTransportConfigRo } from '@teable/openapi'; import { + EmailVerifyCodeType, MailTransporterType, MailType, setSettingMailTransportConfig, @@ -144,8 +145,9 @@ describe.skip('Mail sender (e2e)', () => { }); promises.push(promise2); const emailVerifyCodeEmailOptions = await mailSenderService.sendEmailVerifyCodeEmailOptions({ - title: 'sendEmailVerifyCodeEmail', - message: 'code: 123456', + code: '123456', + expiresIn: '10 minutes', + type: EmailVerifyCodeType.ChangeEmail, }); const mailOptions3 = { ...emailVerifyCodeEmailOptions, diff --git a/apps/nextjs-app/.env.development b/apps/nextjs-app/.env.development index 20aedcce96..7de7546c4b 100644 --- a/apps/nextjs-app/.env.development +++ b/apps/nextjs-app/.env.development @@ -16,9 +16,8 @@ NEXTJS_DIR=../nextjs-app LOG_LEVEL=info PORT=3000 SOCKET_PORT=3001 - PUBLIC_ORIGIN=http://localhost:3000 - +I18N_TYPES_OUTPUT_PATH=./src/types/i18n.generated.ts # DATABASE_URL # @see https://www.prisma.io/docs/reference/database-reference/connection-urls#examples PRISMA_DATABASE_URL=postgresql://teable:teable@127.0.0.1:5432/teable?schema=public&statement_cache_size=1 diff --git a/apps/nextjs-app/src/features/app/components/LanguagePicker.tsx b/apps/nextjs-app/src/features/app/components/LanguagePicker.tsx index e0112484eb..b8381230fb 100644 --- a/apps/nextjs-app/src/features/app/components/LanguagePicker.tsx +++ b/apps/nextjs-app/src/features/app/components/LanguagePicker.tsx @@ -1,3 +1,5 @@ +import { useMutation } from '@tanstack/react-query'; +import { updateUserLang } from '@teable/openapi'; import { Button } from '@teable/ui-lib/shadcn/ui/button'; import { DropdownMenu, @@ -33,15 +35,20 @@ const setCookie = (locale?: string) => { export const LanguagePicker: React.FC<{ className?: string }> = ({ className }) => { const { t, i18n } = useTranslation('common'); + + const { mutateAsync: updateLangMutate } = useMutation({ + mutationFn: (ro: { lang: string }) => updateUserLang(ro), + onSuccess: (_data, variables) => { + setCookie(variables.lang); + i18n.changeLanguage(variables.lang); + toast.message(t('actions.updateSucceed')); + window.location.reload(); + }, + }); + const setLanguage = (value: string) => { - if (value === 'default') { - setCookie(); - } else { - setCookie(value); - i18n.changeLanguage(value); - } - toast.message(t('actions.updateSucceed')); - window.location.reload(); + const lang = value === 'default' ? '' : value; + updateLangMutate({ lang }); }; const currentLanguage = i18n.language.split('-')[0]; diff --git a/apps/nextjs-app/src/features/app/components/notifications/notification-component/LinkNotification.tsx b/apps/nextjs-app/src/features/app/components/notifications/notification-component/LinkNotification.tsx index 713d49bbb8..ca450199eb 100644 --- a/apps/nextjs-app/src/features/app/components/notifications/notification-component/LinkNotification.tsx +++ b/apps/nextjs-app/src/features/app/components/notifications/notification-component/LinkNotification.tsx @@ -1,17 +1,42 @@ -import { NotificationTypeEnum, type NotificationStatesEnum } from '@teable/core'; +import { NotificationTypeEnum } from '@teable/core'; +import type { ILocalization, NotificationStatesEnum } from '@teable/core'; import { type INotificationVo } from '@teable/openapi'; +import { getLocalizationMessage } from '@teable/sdk/context'; +import type { ILocaleFunction } from '@teable/sdk/context/app/i18n'; import Link from 'next/link'; +import { useTranslation } from 'next-i18next'; interface LinkNotificationProps { data: INotificationVo['notifications'][number]; notifyStatus: NotificationStatesEnum; } +const getShowMessage = (data: INotificationVo['notifications'][number], t: ILocaleFunction) => { + const { message, messageI18n } = data; + try { + if (!messageI18n) { + return message; + } + const parsedMessage = JSON.parse(messageI18n); + const { i18nKey = '', context } = parsedMessage as ILocalization; + if (!i18nKey) { + return message; + } + return getLocalizationMessage({ i18nKey, context }, t, 'common'); + } catch (error) { + return message; + } +}; + export const LinkNotification = (props: LinkNotificationProps) => { const { - data: { url, message, notifyType }, + data, + data: { url, notifyType }, } = props; + const { t } = useTranslation(['common']); + const message = getShowMessage(data, t as ILocaleFunction); + return notifyType !== NotificationTypeEnum.ExportBase ? (
{{fromUserName}} hat Sie zum Feld {{fieldName}} eines Datensatzes in {{tableName}} hinzugefügt", + "buttonText": "Datensatz anzeigen" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} hat Sie zu {{refLength}} Datensätzen in {{tableName}} hinzugefügt", + "title": "{{fromUserName}} hat Sie zu {{refLength}} Datensätzen in {{tableName}} hinzugefügt", + "buttonText": "Datensätze anzeigen" + }, + "invite": { + "subject": "{{name}} ({{email}}) hat Sie zu {{resourceAlias}} {{resourceName}} eingeladen - {{brandName}}", + "title": "Einladung zur Zusammenarbeit", + "message": "{{name}} ({{email}}) hat Sie zu {{resourceAlias}} {{resourceName}} eingeladen.", + "buttonText": "Einladung annehmen" + }, + "waitlistInvite": { + "subject": "Willkommen - {{brandName}}", + "title": "Willkommen", + "message": "Sie haben sich erfolgreich auf der Warteliste von {{brandName}} eingetragen. Bitte verwenden Sie den folgenden Einladungscode zur Registrierung: {{code}}. Er kann {{times}} Mal verwendet werden.", + "buttonText": "Registrieren" + }, + "test": { + "subject": "Test-E-Mail - {{brandName}}", + "title": "Test-E-Mail", + "message": "Dies ist eine Test-E-Mail, bitte ignorieren." + }, + "notify": { + "subject": "Benachrichtigung - {{brandName}}", + "title": "Benachrichtigung", + "buttonText": "Anzeigen", + "import": { + "title": "Importergebnis-Benachrichtigung", + "table": { + "aborted": { + "message": "❌ {{tableName}} Import abgebrochen: {{errorMessage}} fehlgeschlagener Zeilenbereich: [{{range}}]. Bitte überprüfen Sie die Daten für diesen Bereich und versuchen Sie es erneut." + }, + "failed": { + "message": "❌ {{tableName}} Import fehlgeschlagen: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} erfolgreich importiert.", + "inplace": "🎉 {{tableName}} erfolgreich inkrementell importiert." + } + } + }, + "recordComment": { + "title": "Datensatzkommentar-Benachrichtigung", + "message": "{{fromUserName}} hat einen Kommentar zu {{recordName}} in {{tableName}} in {{baseName}} abgegeben" + }, + "automation": { + "title": "Automatisierungs-Benachrichtigung", + "failed": { + "title": "Automatisierung {{name}} fehlgeschlagen", + "message": "Ihre Automatisierung {{name}} konnte nicht ausgeführt werden. Klicken Sie auf die Schaltfläche unten, um spezifische Fehler aus der Ausführungshistorie anzuzeigen." + }, + "insufficientCredit": { + "title": "Automatisierung {{name}} aufgrund unzureichenden Guthabens fehlgeschlagen", + "message": "Ihre Automatisierung {{name}} konnte aufgrund unzureichenden Guthabens nicht ausgeführt werden. Bitte aktualisieren Sie Ihr Abonnement oder kontaktieren Sie den Support." + } + }, + "exportBase": { + "title": "Export-Base-Ergebnis-Benachrichtigung", + "success": { + "message": "{{baseName}} erfolgreich exportiert: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ {{baseName}} Export fehlgeschlagen: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "KI-Aufgabe in Tabelle {{tableName}} fehlgeschlagen", + "message": "Die KI-Aufgabe für Feld {{fieldName}} in Tabelle {{tableName}} (Datensatz-ID: {{recordId}}) ist fehlgeschlagen.\n\n{{errorMsg}}\n\nKlicken Sie auf die Schaltfläche unten, um Details anzuzeigen." + } + } + } + } + } }, "waitlist": { "title": "Warteliste", diff --git a/packages/common-i18n/src/locales/en/common.json b/packages/common-i18n/src/locales/en/common.json index 24e7a7d73e..a01efb47d0 100644 --- a/packages/common-i18n/src/locales/en/common.json +++ b/packages/common-i18n/src/locales/en/common.json @@ -670,7 +670,111 @@ "unsubscribeH2": "You're about to unsubscribe from future Teable promotions and product updates. Are you sure you want to unsubscribe?", "subscribeH1": "Confirm Subscribe?", "subscribeH2": "You're about to subscribe from future Teable promotions and product updates. Are you sure you want to subscribe?", - "unsubscribeListTip": "The following users in the current base have unsubscribed, you will no longer be able to send them emails. When importing emails, please place the email addresses in the first column, and name the header \"email\"." + "unsubscribeListTip": "The following users in the current base have unsubscribed, you will no longer be able to send them emails. When importing emails, please place the email addresses in the first column, and name the header \"email\".", + "templates": { + "resetPassword": { + "subject": "Reset Password - {{brandName}}", + "title": "Reset Your Password", + "message": "If you did not request this change, please ignore this email. Otherwise, click the button below to reset your password.", + "buttonText": "Reset Password" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Signup Verification - {{brandName}}", + "title": "Signup Verification", + "message": "Your verification code is {{code}}, please use it within {{expiresIn}} minutes." + }, + "domainVerification": { + "subject": "Domain Verification - {{brandName}}", + "title": "Domain Verification", + "message": "Your verification code is {{code}}, please use it within {{expiresIn}} minutes." + }, + "changeEmailVerification": { + "subject": "Change Email Verification - {{brandName}}", + "title": "Change Email Verification", + "message": "Your verification code is {{code}}, please use it within {{expiresIn}} minutes." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} added you to the {{fieldName}} field of a record in {{tableName}}", + "title": "{{fromUserName}} added you to the {{fieldName}} field of a record in {{tableName}}", + "buttonText": "View record" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} added you to {{refLength}} records in {{tableName}}", + "title": "{{fromUserName}} added you to {{refLength}} records in {{tableName}}", + "buttonText": "View records" + }, + "invite": { + "subject": "{{name}} ({{email}}) invited you to their {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Invitation to Collaborate", + "message": "{{name}} ({{email}}) invited you to their {{resourceAlias}} {{resourceName}}.", + "buttonText": "Accept Invitation" + }, + "waitlistInvite": { + "subject": "Welcome - {{brandName}}", + "title": "Welcome", + "message": "You've successfully joined the waitlist of {{brandName}}, please use the following invite code to register: {{code}}, it can be used {{times}} times.", + "buttonText": "Register" + }, + "test": { + "subject": "Test Email - {{brandName}}", + "title": "Test Email", + "message": "This is a test email, please ignore." + }, + "notify": { + "subject": "Notify - {{brandName}}", + "title": "Notify", + "buttonText": "View", + "import": { + "title": "Import Result Notification", + "table": { + "aborted": { + "message": "❌ {{tableName}} import aborted: {{errorMessage}} failed row range: [{{range}}]. Please check the data for this range and retry." + }, + "failed": { + "message": "❌ {{tableName}} import failed: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} imported successfully.", + "inplace": "🎉 {{tableName}} inplace imported successfully." + } + } + }, + "recordComment": { + "title": "Record Comment Notification", + "message": "{{fromUserName}} made a comment on {{recordName}} in {{tableName}} in {{baseName}}" + }, + "automation": { + "title": "Automation Notification", + "failed": { + "title": "Automation {{name}} failed", + "message": "Your Automation {{name}} has failed to run. Click the button below to view specific errors from the run history." + }, + "insufficientCredit": { + "title": "Automation {{name}} failed due to insufficient credit", + "message": "Your Automation {{name}} has failed to run due to insufficient credit. please upgrade subscription or contact support." + } + }, + "exportBase": { + "title": "Export base result notification", + "success": { + "message": "{{baseName}} Export successfully: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ {{baseName}} exported failed: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "AI task failed in table {{tableName}}", + "message": "The AI task for field {{fieldName}} in table {{tableName}} (Record ID: {{recordId}}) has failed.\n\n{{errorMsg}}\n\nClick the button below to view details." + } + } + } + } + } }, "waitlist": { "title": "Waitlist", diff --git a/packages/common-i18n/src/locales/es/common.json b/packages/common-i18n/src/locales/es/common.json index 50935040af..bb05ce0b94 100644 --- a/packages/common-i18n/src/locales/es/common.json +++ b/packages/common-i18n/src/locales/es/common.json @@ -518,7 +518,111 @@ "unsubscribeH2": "Estás a punto de cancelar la suscripción a futuras promociones y actualizaciones de productos de Teable. ¿Estás seguro de que deseas cancelar la suscripción?", "subscribeH1": "¿Confirmar suscripción?", "subscribeH2": "Estás a punto de suscribirte a futuras promociones y actualizaciones de productos de Teable. ¿Estás seguro de que deseas suscribirte?", - "unsubscribeListTip": "Los siguientes usuarios de la base actual se han dado de baja, ya no podrás enviarles correos electrónicos. Al importar correos electrónicos, coloque las direcciones de correo electrónico en la primera columna y nombre el encabezado \"email\"." + "unsubscribeListTip": "Los siguientes usuarios de la base actual se han dado de baja, ya no podrás enviarles correos electrónicos. Al importar correos electrónicos, coloque las direcciones de correo electrónico en la primera columna y nombre el encabezado \"email\".", + "templates": { + "resetPassword": { + "subject": "Restablecer contraseña - {{brandName}}", + "title": "Restablece tu contraseña", + "message": "Si no solicitaste este cambio, ignora este correo electrónico. De lo contrario, haz clic en el botón a continuación para restablecer tu contraseña.", + "buttonText": "Restablecer contraseña" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Verificación de registro - {{brandName}}", + "title": "Verificación de registro", + "message": "Tu código de verificación es {{code}}, úsalo dentro de {{expiresIn}} minutos." + }, + "domainVerification": { + "subject": "Verificación de dominio - {{brandName}}", + "title": "Verificación de dominio", + "message": "Tu código de un solo uso es: {{code}}, úsalo dentro de {{expiresIn}} minutos." + }, + "changeEmailVerification": { + "subject": "Verificación de cambio de correo electrónico - {{brandName}}", + "title": "Verificación de cambio de correo electrónico", + "message": "Tu código de verificación es {{code}}, úsalo dentro de {{expiresIn}} minutos." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} te agregó al campo {{fieldName}} de un registro en {{tableName}}", + "title": "{{fromUserName}} te agregó al campo {{fieldName}} de un registro en {{tableName}}", + "buttonText": "Ver registro" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} te agregó a {{refLength}} registros en {{tableName}}", + "title": "{{fromUserName}} te agregó a {{refLength}} registros en {{tableName}}", + "buttonText": "Ver registros" + }, + "invite": { + "subject": "{{name}} ({{email}}) te invitó a su {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Invitación a colaborar", + "message": "{{name}} ({{email}}) te invitó a su {{resourceAlias}} {{resourceName}}.", + "buttonText": "Aceptar invitación" + }, + "waitlistInvite": { + "subject": "Bienvenido - {{brandName}}", + "title": "Bienvenido", + "message": "Te has unido exitosamente a la lista de espera de {{brandName}}. Usa el siguiente código de invitación para registrarte: {{code}}, se puede usar {{times}} veces.", + "buttonText": "Registrarse" + }, + "test": { + "subject": "Correo de prueba - {{brandName}}", + "title": "Correo de prueba", + "message": "Este es un correo de prueba, por favor ignóralo." + }, + "notify": { + "subject": "Notificación - {{brandName}}", + "title": "Notificación", + "buttonText": "Ver", + "import": { + "title": "Notificación de resultado de importación", + "table": { + "aborted": { + "message": "❌ Importación de {{tableName}} abortada: {{errorMessage}} rango de filas fallidas: [{{range}}]. Por favor verifica los datos para este rango e intenta nuevamente." + }, + "failed": { + "message": "❌ Importación de {{tableName}} fallida: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} importado exitosamente.", + "inplace": "🎉 {{tableName}} importado incrementalmente exitosamente." + } + } + }, + "recordComment": { + "title": "Notificación de comentario de registro", + "message": "{{fromUserName}} hizo un comentario en {{recordName}} en {{tableName}} en {{baseName}}" + }, + "automation": { + "title": "Notificación de automatización", + "failed": { + "title": "La automatización {{name}} falló", + "message": "Tu automatización {{name}} no se pudo ejecutar. Haz clic en el botón a continuación para ver los errores específicos del historial de ejecución." + }, + "insufficientCredit": { + "title": "La automatización {{name}} falló por crédito insuficiente", + "message": "Tu automatización {{name}} no se pudo ejecutar por crédito insuficiente. Por favor actualiza la suscripción o contacta con soporte." + } + }, + "exportBase": { + "title": "Notificación de resultado de exportación de base", + "success": { + "message": "{{baseName}} exportado exitosamente: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ Exportación de {{baseName}} fallida: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "Tarea de IA falló en la tabla {{tableName}}", + "message": "La tarea de IA para el campo {{fieldName}} en la tabla {{tableName}} (ID de registro: {{recordId}}) ha fallado.\n\n{{errorMsg}}\n\nHaz clic en el botón a continuación para ver los detalles." + } + } + } + } + } }, "waitlist": { "title": "Lista de espera", diff --git a/packages/common-i18n/src/locales/fr/common.json b/packages/common-i18n/src/locales/fr/common.json index 2f23feb338..7fa0e75443 100644 --- a/packages/common-i18n/src/locales/fr/common.json +++ b/packages/common-i18n/src/locales/fr/common.json @@ -473,7 +473,111 @@ "unsubscribeH2": "Vous êtes sur le point de vous désabonner des futures promotions et mises à jour de produits Teable. Êtes-vous sûr de vouloir vous désabonner ?", "subscribeH1": "Confirmer l'abonnement ?", "subscribeH2": "Vous êtes sur le point de vous abonner aux futures promotions et mises à jour de produits Teable. Êtes-vous sûr de vouloir vous abonner ?", - "unsubscribeListTip": "Les utilisateurs suivants de la base actuelle se sont désabonnés, vous ne pourrez plus leur envoyer d'e-mails. Lors de l'importation d'e-mails, veuillez placer les adresses e-mail dans la première colonne et nommer l'en-tête \"email\"." + "unsubscribeListTip": "Les utilisateurs suivants de la base actuelle se sont désabonnés, vous ne pourrez plus leur envoyer d'e-mails. Lors de l'importation d'e-mails, veuillez placer les adresses e-mail dans la première colonne et nommer l'en-tête \"email\".", + "templates": { + "resetPassword": { + "subject": "Réinitialiser le mot de passe - {{brandName}}", + "title": "Réinitialisez votre mot de passe", + "message": "Si vous n'avez pas demandé ce changement, veuillez ignorer cet e-mail. Sinon, cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe.", + "buttonText": "Réinitialiser le mot de passe" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Vérification d'inscription - {{brandName}}", + "title": "Vérification d'inscription", + "message": "Votre code de vérification est {{code}}, veuillez l'utiliser dans {{expiresIn}} minutes." + }, + "domainVerification": { + "subject": "Vérification de domaine - {{brandName}}", + "title": "Vérification de domaine", + "message": "Votre code à usage unique est : {{code}}, veuillez l'utiliser dans {{expiresIn}} minutes." + }, + "changeEmailVerification": { + "subject": "Vérification de changement d'e-mail - {{brandName}}", + "title": "Vérification de changement d'e-mail", + "message": "Votre code de vérification est {{code}}, veuillez l'utiliser dans {{expiresIn}} minutes." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} vous a ajouté au champ {{fieldName}} d'un enregistrement dans {{tableName}}", + "title": "{{fromUserName}} vous a ajouté au champ {{fieldName}} d'un enregistrement dans {{tableName}}", + "buttonText": "Voir l'enregistrement" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} vous a ajouté à {{refLength}} enregistrements dans {{tableName}}", + "title": "{{fromUserName}} vous a ajouté à {{refLength}} enregistrements dans {{tableName}}", + "buttonText": "Voir les enregistrements" + }, + "invite": { + "subject": "{{name}} ({{email}}) vous a invité à leur {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Invitation à collaborer", + "message": "{{name}} ({{email}}) vous a invité à leur {{resourceAlias}} {{resourceName}}.", + "buttonText": "Accepter l'invitation" + }, + "waitlistInvite": { + "subject": "Bienvenue - {{brandName}}", + "title": "Bienvenue", + "message": "Vous avez rejoint avec succès la liste d'attente de {{brandName}}. Veuillez utiliser le code d'invitation suivant pour vous inscrire: {{code}}, il peut être utilisé {{times}} fois.", + "buttonText": "S'inscrire" + }, + "test": { + "subject": "E-mail de test - {{brandName}}", + "title": "E-mail de test", + "message": "Ceci est un e-mail de test, veuillez l'ignorer." + }, + "notify": { + "subject": "Notification - {{brandName}}", + "title": "Notification", + "buttonText": "Voir", + "import": { + "title": "Notification de résultat d'importation", + "table": { + "aborted": { + "message": "❌ Importation de {{tableName}} interrompue: {{errorMessage}} plage de lignes échouées: [{{range}}]. Veuillez vérifier les données de cette plage et réessayer." + }, + "failed": { + "message": "❌ Importation de {{tableName}} échouée: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} importé avec succès.", + "inplace": "🎉 {{tableName}} importé en place avec succès." + } + } + }, + "recordComment": { + "title": "Notification de commentaire d'enregistrement", + "message": "{{fromUserName}} a fait un commentaire sur {{recordName}} dans {{tableName}} dans {{baseName}}" + }, + "automation": { + "title": "Notification d'automatisation", + "failed": { + "title": "L'automatisation {{name}} a échoué", + "message": "Votre automatisation {{name}} n'a pas pu s'exécuter. Cliquez sur le bouton ci-dessous pour voir les erreurs spécifiques de l'historique d'exécution." + }, + "insufficientCredit": { + "title": "L'automatisation {{name}} a échoué en raison d'un crédit insuffisant", + "message": "Votre automatisation {{name}} n'a pas pu s'exécuter en raison d'un crédit insuffisant. Veuillez mettre à niveau votre abonnement ou contacter le support." + } + }, + "exportBase": { + "title": "Notification de résultat d'exportation de base", + "success": { + "message": "{{baseName}} exporté avec succès: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ Exportation de {{baseName}} échouée: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "Tâche IA échouée dans la table {{tableName}}", + "message": "La tâche IA pour le champ {{fieldName}} dans la table {{tableName}} (ID d'enregistrement: {{recordId}}) a échoué.\n\n{{errorMsg}}\n\nCliquez sur le bouton ci-dessous pour voir les détails." + } + } + } + } + } }, "waitlist": { "title": "Liste d'attente", @@ -503,6 +607,7 @@ "sendErrorToAI": "Envoyer l'erreur à AI" } }, + "base": { "deleteTip": "Êtes-vous sûr de vouloir supprimer \"{{name}}\" base?" } diff --git a/packages/common-i18n/src/locales/it/common.json b/packages/common-i18n/src/locales/it/common.json index cc222cdc45..5962e05f19 100644 --- a/packages/common-i18n/src/locales/it/common.json +++ b/packages/common-i18n/src/locales/it/common.json @@ -535,7 +535,111 @@ "unsubscribeH2": "Stai per annullare l'iscrizione alle future promozioni e aggiornamenti dei prodotti Teable. Sei sicuro di voler annullare l'iscrizione?", "subscribeH1": "Confermare l'iscrizione?", "subscribeH2": "Stai per iscriverti alle future promozioni e aggiornamenti dei prodotti Teable. Sei sicuro di volerti iscrivere?", - "unsubscribeListTip": "I seguenti utenti nella base corrente si sono disiscritti, non potrai più inviare loro e-mail. Durante l'importazione delle e-mail, posizionare gli indirizzi e-mail nella prima colonna e nominare l'intestazione \"email\"." + "unsubscribeListTip": "I seguenti utenti nella base corrente si sono disiscritti, non potrai più inviare loro e-mail. Durante l'importazione delle e-mail, posizionare gli indirizzi e-mail nella prima colonna e nominare l'intestazione \"email\".", + "templates": { + "resetPassword": { + "subject": "Reimposta password - {{brandName}}", + "title": "Reimposta la tua password", + "message": "Se non hai richiesto questa modifica, ignora questa email. Altrimenti, fai clic sul pulsante qui sotto per reimpostare la tua password.", + "buttonText": "Reimposta password" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Verifica registrazione - {{brandName}}", + "title": "Verifica registrazione", + "message": "Il tuo codice di verifica è {{code}}, usalo entro {{expiresIn}} minuti." + }, + "domainVerification": { + "subject": "Verifica dominio - {{brandName}}", + "title": "Verifica dominio", + "message": "Il tuo codice monouso è: {{code}}, usalo entro {{expiresIn}} minuti." + }, + "changeEmailVerification": { + "subject": "Verifica cambio email - {{brandName}}", + "title": "Verifica cambio email", + "message": "Il tuo codice di verifica è {{code}}, usalo entro {{expiresIn}} minuti." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} ti ha aggiunto al campo {{fieldName}} di un record in {{tableName}}", + "title": "{{fromUserName}} ti ha aggiunto al campo {{fieldName}} di un record in {{tableName}}", + "buttonText": "Visualizza record" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} ti ha aggiunto a {{refLength}} record in {{tableName}}", + "title": "{{fromUserName}} ti ha aggiunto a {{refLength}} record in {{tableName}}", + "buttonText": "Visualizza record" + }, + "invite": { + "subject": "{{name}} ({{email}}) ti ha invitato al loro {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Invito a collaborare", + "message": "{{name}} ({{email}}) ti ha invitato al loro {{resourceAlias}} {{resourceName}}.", + "buttonText": "Accetta invito" + }, + "waitlistInvite": { + "subject": "Benvenuto - {{brandName}}", + "title": "Benvenuto", + "message": "Ti sei unito con successo alla lista d'attesa di {{brandName}}. Usa il seguente codice di invito per registrarti: {{code}}, può essere utilizzato {{times}} volte.", + "buttonText": "Registrati" + }, + "test": { + "subject": "Email di test - {{brandName}}", + "title": "Email di test", + "message": "Questa è un'email di test, ignorala." + }, + "notify": { + "subject": "Notifica - {{brandName}}", + "title": "Notifica", + "buttonText": "Visualizza", + "import": { + "title": "Notifica risultato importazione", + "table": { + "aborted": { + "message": "❌ Importazione di {{tableName}} interrotta: {{errorMessage}} intervallo di righe fallite: [{{range}}]. Verifica i dati di questo intervallo e riprova." + }, + "failed": { + "message": "❌ Importazione di {{tableName}} fallita: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} importato con successo.", + "inplace": "🎉 {{tableName}} importato in loco con successo." + } + } + }, + "recordComment": { + "title": "Notifica commento record", + "message": "{{fromUserName}} ha fatto un commento su {{recordName}} in {{tableName}} in {{baseName}}" + }, + "automation": { + "title": "Notifica automazione", + "failed": { + "title": "L'automazione {{name}} è fallita", + "message": "La tua automazione {{name}} non è riuscita ad essere eseguita. Fai clic sul pulsante qui sotto per visualizzare errori specifici dalla cronologia di esecuzione." + }, + "insufficientCredit": { + "title": "L'automazione {{name}} è fallita a causa di credito insufficiente", + "message": "La tua automazione {{name}} non è riuscita ad essere eseguita a causa di credito insufficiente. Aggiorna l'abbonamento o contatta il supporto." + } + }, + "exportBase": { + "title": "Notifica risultato esportazione base", + "success": { + "message": "{{baseName}} esportato con successo: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ Esportazione di {{baseName}} fallita: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "Attività AI fallita nella tabella {{tableName}}", + "message": "L'attività AI per il campo {{fieldName}} nella tabella {{tableName}} (ID record: {{recordId}}) è fallita.\n\n{{errorMsg}}\n\nFai clic sul pulsante qui sotto per visualizzare i dettagli." + } + } + } + } + } }, "waitlist": { "title": "Lista d'attesa", @@ -556,7 +660,7 @@ "no": "No", "generateCode": "Genera codice di invito", "count": "Quantità", - "times": "Usages", + "times": "Utilizzi", "generate": "Genera", "code": "Codice di invito", "inviteSuccess": "Invito riuscito", @@ -565,6 +669,7 @@ "sendErrorToAI": "Invia errore a AI" } }, + "base": { "deleteTip": "Sei sicuro di voler eliminare \"{{name}}\" base?" } diff --git a/packages/common-i18n/src/locales/ja/common.json b/packages/common-i18n/src/locales/ja/common.json index de6bdad572..406c7dba86 100644 --- a/packages/common-i18n/src/locales/ja/common.json +++ b/packages/common-i18n/src/locales/ja/common.json @@ -494,7 +494,111 @@ "unsubscribeH2": "今後のTeable のプロモーションや製品アップデート情報の配信を解除しようとしています。登録解除してもよろしいですか?", "subscribeH1": "登録を確認しますか?", "subscribeH2": "今後のTeable のプロモーションや製品アップデート情報の配信を登録しようとしています。登録してもよろしいですか?", - "unsubscribeListTip": "現在のベースの以下のユーザーが購読を解除しました。これらのユーザーにはメールを送信できなくなります。メールをインポートする際は、メールアドレスを最初の列に配置し、ヘッダーに「email」という名前を付けてください。" + "unsubscribeListTip": "現在のベースの以下のユーザーが購読を解除しました。これらのユーザーにはメールを送信できなくなります。メールをインポートする際は、メールアドレスを最初の列に配置し、ヘッダーに「email」という名前を付けてください。", + "templates": { + "resetPassword": { + "subject": "パスワードをリセット - {{brandName}}", + "title": "パスワードをリセット", + "message": "この変更をリクエストしていない場合は、このメールを無視してください。それ以外の場合は、下のボタンをクリックしてパスワードをリセットしてください。", + "buttonText": "パスワードをリセット" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "登録確認 - {{brandName}}", + "title": "登録確認", + "message": "確認コードは{{code}}です。{{expiresIn}}分以内に使用してください。" + }, + "domainVerification": { + "subject": "ドメイン確認 - {{brandName}}", + "title": "ドメイン確認", + "message": "あなたのワンタイムコードは:{{code}}です。{{expiresIn}}分以内に使用してください。" + }, + "changeEmailVerification": { + "subject": "メール変更確認 - {{brandName}}", + "title": "メール変更確認", + "message": "確認コードは{{code}}です。{{expiresIn}}分以内に使用してください。" + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}}があなたを{{tableName}}のレコードの{{fieldName}}フィールドに追加しました", + "title": "{{fromUserName}}があなたを{{tableName}}のレコードの{{fieldName}}フィールドに追加しました", + "buttonText": "レコードを表示" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}}があなたを{{tableName}}の{{refLength}}件のレコードに追加しました", + "title": "{{fromUserName}}があなたを{{tableName}}{{refLength}}件のレコードに追加しました", + "buttonText": "レコードを表示" + }, + "invite": { + "subject": "{{name}} ({{email}})があなたを{{resourceAlias}} {{resourceName}}に招待しました - {{brandName}}", + "title": "コラボレーションへの招待", + "message": "{{name}} ({{email}})があなたを{{resourceAlias}} {{resourceName}}に招待しました。", + "buttonText": "招待を承諾" + }, + "waitlistInvite": { + "subject": "ようこそ - {{brandName}}", + "title": "ようこそ", + "message": "{{brandName}}のウェイトリストに登録されました。次の招待コードを使用して登録してください:{{code}}、{{times}}回使用できます。", + "buttonText": "登録" + }, + "test": { + "subject": "テストメール - {{brandName}}", + "title": "テストメール", + "message": "これはテストメールです。無視してください。" + }, + "notify": { + "subject": "通知 - {{brandName}}", + "title": "通知", + "buttonText": "表示", + "import": { + "title": "インポート結果通知", + "table": { + "aborted": { + "message": "❌ {{tableName}}のインポートが中止されました:{{errorMessage}} 失敗した行の範囲:[{{range}}]。この範囲のデータを確認して再試行してください。" + }, + "failed": { + "message": "❌ {{tableName}}のインポートが失敗しました:{{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}}が正常にインポートされました。", + "inplace": "🎉 {{tableName}}が正常にインプレースインポートされました。" + } + } + }, + "recordComment": { + "title": "レコードコメント通知", + "message": "{{fromUserName}}が{{baseName}}の{{tableName}}の{{recordName}}にコメントしました" + }, + "automation": { + "title": "自動化通知", + "failed": { + "title": "自動化{{name}}が失敗しました", + "message": "自動化{{name}}の実行に失敗しました。下のボタンをクリックして、実行履歴から具体的なエラーを表示してください。" + }, + "insufficientCredit": { + "title": "自動化{{name}}がクレジット不足により失敗しました", + "message": "自動化{{name}}がクレジット不足により実行に失敗しました。サブスクリプションをアップグレードするか、サポートに連絡してください。" + } + }, + "exportBase": { + "title": "ベースエクスポート結果通知", + "success": { + "message": "{{baseName}}が正常にエクスポートされました:🗂️ {{name}}" + }, + "failed": { + "message": "❌ {{baseName}}のエクスポートが失敗しました:{{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "テーブル{{tableName}}でAIタスクが失敗しました", + "message": "テーブル{{tableName}}のフィールド{{fieldName}}のAIタスク(レコードID:{{recordId}})が失敗しました。\n\n{{errorMsg}}\n\n下のボタンをクリックして詳細を表示してください。" + } + } + } + } + } }, "waitlist": { "title": "待機リスト", diff --git a/packages/common-i18n/src/locales/ru/common.json b/packages/common-i18n/src/locales/ru/common.json index 587a7ff6b7..1a0e037cad 100644 --- a/packages/common-i18n/src/locales/ru/common.json +++ b/packages/common-i18n/src/locales/ru/common.json @@ -496,7 +496,111 @@ "unsubscribeH2": "Вы собираетесь отписаться от будущих рекламных акций и обновлений продуктов Teable. Вы уверены, что хотите отписаться?", "subscribeH1": "Подтвердить подписку?", "subscribeH2": "Вы собираетесь подписаться на будущие рекламные акции и обновления продуктов Teable. Вы уверены, что хотите подписаться?", - "unsubscribeListTip": "Следующие пользователи текущей базы отписались, вы больше не сможете отправлять им электронные письма. При импорте электронных писем, пожалуйста, поместите адреса электронной почты в первый столбец и назовите заголовок \"email\"." + "unsubscribeListTip": "Следующие пользователи текущей базы отписались, вы больше не сможете отправлять им электронные письма. При импорте электронных писем, пожалуйста, поместите адреса электронной почты в первый столбец и назовите заголовок \"email\".", + "templates": { + "resetPassword": { + "subject": "Сброс пароля - {{brandName}}", + "title": "Сбросьте ваш пароль", + "message": "Если вы не запрашивали это изменение, проигнорируйте это письмо. В противном случае нажмите кнопку ниже, чтобы сбросить пароль.", + "buttonText": "Сбросить пароль" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Подтверждение регистрации - {{brandName}}", + "title": "Подтверждение регистрации", + "message": "Ваш код подтверждения {{code}}, используйте его в течение {{expiresIn}} минут." + }, + "domainVerification": { + "subject": "Подтверждение домена - {{brandName}}", + "title": "Подтверждение домена", + "message": "Ваш одноразовый код: {{code}}, используйте его в течение {{expiresIn}} минут." + }, + "changeEmailVerification": { + "subject": "Подтверждение смены email - {{brandName}}", + "title": "Подтверждение смены email", + "message": "Ваш код подтверждения {{code}}, используйте его в течение {{expiresIn}} минут." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} добавил вас в поле {{fieldName}} записи в {{tableName}}", + "title": "{{fromUserName}} добавил вас в поле {{fieldName}} записи в {{tableName}}", + "buttonText": "Посмотреть запись" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} добавил вас в {{refLength}} записей в {{tableName}}", + "title": "{{fromUserName}} добавил вас в {{refLength}} записей в {{tableName}}", + "buttonText": "Посмотреть записи" + }, + "invite": { + "subject": "{{name}} ({{email}}) пригласил вас в {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Приглашение к сотрудничеству", + "message": "{{name}} ({{email}}) пригласил вас в {{resourceAlias}} {{resourceName}}.", + "buttonText": "Принять приглашение" + }, + "waitlistInvite": { + "subject": "Добро пожаловать - {{brandName}}", + "title": "Добро пожаловать", + "message": "Вы успешно присоединились к списку ожидания {{brandName}}. Используйте следующий код приглашения для регистрации: {{code}}, его можно использовать {{times}} раз.", + "buttonText": "Зарегистрироваться" + }, + "test": { + "subject": "Тестовое письмо - {{brandName}}", + "title": "Тестовое письмо", + "message": "Это тестовое письмо, проигнорируйте его." + }, + "notify": { + "subject": "Уведомление - {{brandName}}", + "title": "Уведомление", + "buttonText": "Посмотреть", + "import": { + "title": "Уведомление о результате импорта", + "table": { + "aborted": { + "message": "❌ Импорт {{tableName}} прерван: {{errorMessage}} диапазон неудачных строк: [{{range}}]. Проверьте данные этого диапазона и повторите попытку." + }, + "failed": { + "message": "❌ Импорт {{tableName}} не удался: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} успешно импортирована.", + "inplace": "🎉 {{tableName}} успешно импортирована на месте." + } + } + }, + "recordComment": { + "title": "Уведомление о комментарии к записи", + "message": "{{fromUserName}} оставил комментарий к {{recordName}} в {{tableName}} в {{baseName}}" + }, + "automation": { + "title": "Уведомление об автоматизации", + "failed": { + "title": "Автоматизация {{name}} не удалась", + "message": "Ваша автоматизация {{name}} не смогла выполниться. Нажмите кнопку ниже, чтобы просмотреть конкретные ошибки из истории выполнения." + }, + "insufficientCredit": { + "title": "Автоматизация {{name}} не удалась из-за недостатка кредита", + "message": "Ваша автоматизация {{name}} не смогла выполниться из-за недостатка кредита. Обновите подписку или обратитесь в поддержку." + } + }, + "exportBase": { + "title": "Уведомление о результате экспорта базы", + "success": { + "message": "{{baseName}} успешно экспортирована: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ Экспорт {{baseName}} не удался: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "Задача ИИ не удалась в таблице {{tableName}}", + "message": "Задача ИИ для поля {{fieldName}} в таблице {{tableName}} (ID записи: {{recordId}}) не удалась.\n\n{{errorMsg}}\n\nНажмите кнопку ниже, чтобы просмотреть детали." + } + } + } + } + } }, "waitlist": { "title": "Список ожидания", @@ -526,6 +630,7 @@ "sendErrorToAI": "Отправить ошибку в AI" } }, + "base": { "deleteTip": "Вы уверены, что хотите удалить \"{{name}}\" базу?" } diff --git a/packages/common-i18n/src/locales/tr/common.json b/packages/common-i18n/src/locales/tr/common.json index ff9fa3d261..244617ecf9 100644 --- a/packages/common-i18n/src/locales/tr/common.json +++ b/packages/common-i18n/src/locales/tr/common.json @@ -511,7 +511,111 @@ "unsubscribeH2": "Gelecekteki Teable promosyonları ve ürün güncellemelerinden aboneliğinizi iptal etmek üzeresiniz. Abonelikten çıkmak istediğinizden emin misiniz?", "subscribeH1": "Aboneliği onaylıyor musunuz?", "subscribeH2": "Gelecekteki Teable promosyonları ve ürün güncellemelerine abone olmak üzeresiniz. Abone olmak istediğinizden emin misiniz?", - "unsubscribeListTip": "Mevcut tabanındaki aşağıdaki kullanıcılar abonelikten çıkmıştır, artık onlara e-posta gönderemeyeceksiniz. E-postaları içe aktarırken, lütfen e-posta adreslerini ilk sütuna yerleştirin ve başlığı \"email\" olarak adlandırın." + "unsubscribeListTip": "Mevcut tabanındaki aşağıdaki kullanıcılar abonelikten çıkmıştır, artık onlara e-posta gönderemeyeceksiniz. E-postaları içe aktarırken, lütfen e-posta adreslerini ilk sütuna yerleştirin ve başlığı \"email\" olarak adlandırın.", + "templates": { + "resetPassword": { + "subject": "Şifreyi Sıfırla - {{brandName}}", + "title": "Şifrenizi sıfırlayın", + "message": "Bu değişikliği talep etmediyseniz, lütfen bu e-postayı görmezden gelin. Aksi takdirde, şifrenizi sıfırlamak için aşağıdaki düğmeye tıklayın.", + "buttonText": "Şifreyi Sıfırla" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Kayıt Doğrulama - {{brandName}}", + "title": "Kayıt Doğrulama", + "message": "Doğrulama kodunuz {{code}}, lütfen {{expiresIn}} dakika içinde kullanın." + }, + "domainVerification": { + "subject": "Alan Adı Doğrulama - {{brandName}}", + "title": "Alan Adı Doğrulama", + "message": "Tek kullanımlık kodunuz: {{code}}, lütfen {{expiresIn}} dakika içinde kullanın." + }, + "changeEmailVerification": { + "subject": "E-posta Değiştirme Doğrulama - {{brandName}}", + "title": "E-posta Değiştirme Doğrulama", + "message": "Doğrulama kodunuz {{code}}, lütfen {{expiresIn}} dakika içinde kullanın." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} sizi {{tableName}} tablosundaki bir kaydın {{fieldName}} alanına ekledi", + "title": "{{fromUserName}} sizi {{tableName}} tablosundaki bir kaydın {{fieldName}} alanına ekledi", + "buttonText": "Kaydı görüntüle" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} sizi {{tableName}} tablosundaki {{refLength}} kayda ekledi", + "title": "{{fromUserName}} sizi {{tableName}} tablosundaki {{refLength}} kayda ekledi", + "buttonText": "Kayıtları görüntüle" + }, + "invite": { + "subject": "{{name}} ({{email}}) sizi {{resourceAlias}} {{resourceName}}'e davet etti - {{brandName}}", + "title": "İşbirliğine davet", + "message": "{{name}} ({{email}}) sizi {{resourceAlias}} {{resourceName}}'e davet etti.", + "buttonText": "Daveti kabul et" + }, + "waitlistInvite": { + "subject": "Hoş geldiniz - {{brandName}}", + "title": "Hoş geldiniz", + "message": "{{brandName}} bekleme listesine başarıyla katıldınız. Kayıt olmak için aşağıdaki davet kodunu kullanın: {{code}}, {{times}} kez kullanılabilir.", + "buttonText": "Kayıt ol" + }, + "test": { + "subject": "Test E-postası - {{brandName}}", + "title": "Test E-postası", + "message": "Bu bir test e-postasıdır, lütfen görmezden gelin." + }, + "notify": { + "subject": "Bildirim - {{brandName}}", + "title": "Bildirim", + "buttonText": "Görüntüle", + "import": { + "title": "İçe Aktarma Sonuç Bildirimi", + "table": { + "aborted": { + "message": "❌ {{tableName}} içe aktarma iptal edildi: {{errorMessage}} başarısız satır aralığı: [{{range}}]. Lütfen bu aralıktaki verileri kontrol edin ve tekrar deneyin." + }, + "failed": { + "message": "❌ {{tableName}} içe aktarma başarısız oldu: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} başarıyla içe aktarıldı.", + "inplace": "🎉 {{tableName}} yerinde başarıyla içe aktarıldı." + } + } + }, + "recordComment": { + "title": "Kayıt Yorumu Bildirimi", + "message": "{{fromUserName}}, {{baseName}} içindeki {{tableName}} tablosundaki {{recordName}} kaydına yorum yaptı" + }, + "automation": { + "title": "Otomasyon Bildirimi", + "failed": { + "title": "Otomasyon {{name}} başarısız oldu", + "message": "Otomasyonunuz {{name}} çalıştırılamadı. Çalıştırma geçmişinden belirli hataları görmek için aşağıdaki düğmeye tıklayın." + }, + "insufficientCredit": { + "title": "Otomasyon {{name}} yetersiz kredi nedeniyle başarısız oldu", + "message": "Otomasyonunuz {{name}} yetersiz kredi nedeniyle çalıştırılamadı. Lütfen aboneliği yükseltin veya destekle iletişime geçin." + } + }, + "exportBase": { + "title": "Veritabanı Dışa Aktarma Sonuç Bildirimi", + "success": { + "message": "{{baseName}} başarıyla dışa aktarıldı: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ {{baseName}} dışa aktarma başarısız oldu: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "AI görevi {{tableName}} tablosunda başarısız oldu", + "message": "{{tableName}} tablosundaki {{fieldName}} alanı için AI görevi (Kayıt ID: {{recordId}}) başarısız oldu.\n\n{{errorMsg}}\n\nAyrıntıları görmek için aşağıdaki düğmeye tıklayın." + } + } + } + } + } }, "waitlist": { "title": "Bekleme Listesi", diff --git a/packages/common-i18n/src/locales/uk/common.json b/packages/common-i18n/src/locales/uk/common.json index 2bc3d4b01a..35888091b5 100644 --- a/packages/common-i18n/src/locales/uk/common.json +++ b/packages/common-i18n/src/locales/uk/common.json @@ -531,7 +531,111 @@ "unsubscribeH2": "Ви збираєтеся відписатися від майбутніх рекламних акцій та оновлень продуктів Teable. Ви впевнені, що хочете відписатися?", "subscribeH1": "Підтвердити підписку?", "subscribeH2": "Ви збираєтеся підписатися на майбутні рекламні акції та оновлення продуктів Teable. Ви впевнені, що хочете підписатися?", - "unsubscribeListTip": "Наступні користувачі поточної бази відписалися, ви більше не зможете надсилати їм електронні листи. Під час імпорту електронних листів, будь ласка, розмістіть адреси електронної пошти в першому стовпці та назвіть заголовок \"email\"." + "unsubscribeListTip": "Наступні користувачі поточної бази відписалися, ви більше не зможете надсилати їм електронні листи. Під час імпорту електронних листів, будь ласка, розмістіть адреси електронної пошти в першому стовпці та назвіть заголовок \"email\".", + "templates": { + "resetPassword": { + "subject": "Скинути пароль - {{brandName}}", + "title": "Скиньте ваш пароль", + "message": "Якщо ви не запитували цю зміну, будь ласка, проігноруйте цей лист. В іншому випадку натисніть кнопку нижче, щоб скинути пароль.", + "buttonText": "Скинути пароль" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "Підтвердження реєстрації - {{brandName}}", + "title": "Підтвердження реєстрації", + "message": "Ваш код підтвердження {{code}}, будь ласка, використайте його протягом {{expiresIn}} хвилин." + }, + "domainVerification": { + "subject": "Підтвердження домену - {{brandName}}", + "title": "Підтвердження домену", + "message": "Ваш одноразовий код: {{code}}, будь ласка, використайте його протягом {{expiresIn}} хвилин." + }, + "changeEmailVerification": { + "subject": "Підтвердження зміни електронної пошти - {{brandName}}", + "title": "Підтвердження зміни електронної пошти", + "message": "Ваш код підтвердження {{code}}, будь ласка, використайте його протягом {{expiresIn}} хвилин." + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} додав вас до поля {{fieldName}} запису в {{tableName}}", + "title": "{{fromUserName}} додав вас до поля {{fieldName}} запису в {{tableName}}", + "buttonText": "Переглянути запис" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} додав вас до {{refLength}} записів у {{tableName}}", + "title": "{{fromUserName}} додав вас до {{refLength}} записів у {{tableName}}", + "buttonText": "Переглянути записи" + }, + "invite": { + "subject": "{{name}} ({{email}}) запросив вас до {{resourceAlias}} {{resourceName}} - {{brandName}}", + "title": "Запрошення до співпраці", + "message": "{{name}} ({{email}}) запросив вас до {{resourceAlias}} {{resourceName}}.", + "buttonText": "Прийняти запрошення" + }, + "waitlistInvite": { + "subject": "Ласкаво просимо - {{brandName}}", + "title": "Ласкаво просимо", + "message": "Ви успішно приєдналися до списку очікування {{brandName}}. Будь ласка, використовуйте наступний код запрошення для реєстрації: {{code}}, його можна використати {{times}} разів.", + "buttonText": "Зареєструватися" + }, + "test": { + "subject": "Тестовий лист - {{brandName}}", + "title": "Тестовий лист", + "message": "Це тестовий лист, будь ласка, проігноруйте його." + }, + "notify": { + "subject": "Сповіщення - {{brandName}}", + "title": "Сповіщення", + "buttonText": "Переглянути", + "import": { + "title": "Сповіщення про результат імпорту", + "table": { + "aborted": { + "message": "❌ Імпорт {{tableName}} перервано: {{errorMessage}} діапазон невдалих рядків: [{{range}}]. Будь ласка, перевірте дані цього діапазону та спробуйте знову." + }, + "failed": { + "message": "❌ Імпорт {{tableName}} не вдався: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} успішно імпортовано.", + "inplace": "🎉 {{tableName}} успішно імпортовано на місці." + } + } + }, + "recordComment": { + "title": "Сповіщення про коментар до запису", + "message": "{{fromUserName}} залишив коментар до {{recordName}} у {{tableName}} у {{baseName}}" + }, + "automation": { + "title": "Сповіщення про автоматизацію", + "failed": { + "title": "Автоматизація {{name}} не вдалася", + "message": "Ваша автоматизація {{name}} не змогла виконатися. Натисніть кнопку нижче, щоб переглянути конкретні помилки з історії виконання." + }, + "insufficientCredit": { + "title": "Автоматизація {{name}} не вдалася через недостатній кредит", + "message": "Ваша автоматизація {{name}} не змогла виконатися через недостатній кредит. Будь ласка, оновіть підписку або зверніться до підтримки." + } + }, + "exportBase": { + "title": "Сповіщення про результат експорту бази", + "success": { + "message": "{{baseName}} успішно експортовано: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ Експорт {{baseName}} не вдався: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "Завдання ШІ не вдалося в таблиці {{tableName}}", + "message": "Завдання ШІ для поля {{fieldName}} в таблиці {{tableName}} (ID запису: {{recordId}}) не вдалося.\n\n{{errorMsg}}\n\nНатисніть кнопку нижче, щоб переглянути деталі." + } + } + } + } + } }, "waitlist": { "title": "Черга", @@ -561,6 +665,7 @@ "sendErrorToAI": "Надіслати помилку в AI" } }, + "base": { "deleteTip": "Ви впевнені, що хочете видалити \"{{name}}\" базу?" } diff --git a/packages/common-i18n/src/locales/zh/common.json b/packages/common-i18n/src/locales/zh/common.json index 7b3107a085..344d1291b2 100644 --- a/packages/common-i18n/src/locales/zh/common.json +++ b/packages/common-i18n/src/locales/zh/common.json @@ -667,7 +667,111 @@ "unsubscribeH2": "您即将取消订阅 Teable 未来的促销和产品更新信息。您确定要取消订阅吗?", "subscribeH1": "确认订阅", "subscribeH2": "您即将订阅 Teable 未来的促销和产品更新信息。您确定要订阅吗?", - "unsubscribeListTip": "当前数据库的以下用户已取消订阅,您将无法再向他们发送电子邮件。导入电子邮件时,请将电子邮件地址放在第一列,并将标题命名为 email。" + "unsubscribeListTip": "当前数据库的以下用户已取消订阅,您将无法再向他们发送电子邮件。导入电子邮件时,请将电子邮件地址放在第一列,并将标题命名为 email。", + "templates": { + "resetPassword": { + "subject": "重置密码 - {{brandName}}", + "title": "重置您的密码", + "message": "如果您没有请求此更改,请忽略此电子邮件。否则,请单击下面的按钮重置您的密码。", + "buttonText": "重置密码" + }, + "emailVerifyCode": { + "signupVerification": { + "subject": "注册验证 - {{brandName}}", + "title": "注册验证", + "message": "您的验证码是:{{code}},请在 {{expiresIn}} 分钟内使用。" + }, + "domainVerification": { + "subject": "域名验证 - {{brandName}}", + "title": "域名验证", + "message": "您的一次性验证码是:{{code}},请在 {{expiresIn}} 分钟内使用。" + }, + "changeEmailVerification": { + "subject": "更改电子邮件验证 - {{brandName}}", + "title": "更改电子邮件验证", + "message": "您的验证码是:{{code}},请在 {{expiresIn}} 分钟内使用。" + } + }, + "collaboratorCellTag": { + "subject": "{{fromUserName}} 添加您到 {{tableName}} 的 {{fieldName}} 字段中的记录", + "title": "{{fromUserName}} 添加您到 {{tableName}}{{fieldName}} 字段中的记录", + "buttonText": "查看记录" + }, + "collaboratorMultiRowTag": { + "subject": "{{fromUserName}} 添加您到 {{tableName}} 的 {{refLength}} 条记录", + "title": "{{fromUserName}} 添加您到 {{tableName}}{{refLength}} 条记录", + "buttonText": "查看记录" + }, + "invite": { + "subject": "{{name}} ({{email}}) 邀请您到他们的 {{resourceAlias}} {{resourceName}}", + "title": "邀请您协作", + "message": "{{name}} ({{email}}) 邀请您到他们的 {{resourceAlias}} {{resourceName}}。", + "buttonText": "接受邀请" + }, + "waitlistInvite": { + "subject": "欢迎 - {{brandName}}", + "title": "欢迎", + "message": "您已成功加入 {{brandName}} 的等待列表,请使用以下邀请码注册:{{code}},该邀请码可使用 {{times}} 次。", + "buttonText": "注册" + }, + "test": { + "subject": "测试邮件 - {{brandName}}", + "title": "测试邮件", + "message": "这是一封测试邮件,请忽略。" + }, + "notify": { + "subject": "通知 - {{brandName}}", + "title": "通知", + "buttonText": "查看", + "import": { + "title": "导入结果通知", + "table": { + "aborted": { + "message": "❌ {{tableName}} 导入中断: {{errorMessage}} 失败行范围: [{{range}}]. 请检查此范围内的数据并重试。" + }, + "failed": { + "message": "❌ {{tableName}} 导入失败: {{errorMessage}}" + }, + "success": { + "message": "🎉 {{tableName}} 导入成功。", + "inplace": "🎉 {{tableName}} 增量导入成功。" + } + } + }, + "recordComment": { + "title": "记录评论通知", + "message": "{{fromUserName}} 在数据库 {{baseName}} 的 {{tableName}} 的 {{recordName}} 上评论了" + }, + "automation": { + "title": "自动化通知", + "failed": { + "title": "自动化 {{name}} 运行失败", + "message": "自动化 {{name}} 运行失败。点击下面的按钮查看具体错误。" + }, + "insufficientCredit": { + "title": "自动化 {{name}} 运行失败,由于算力不足", + "message": "自动化 {{name}} 运行失败,由于算力不足。请升级订阅或联系支持。" + } + }, + "exportBase": { + "title": "导出数据库结果通知", + "success": { + "message": "{{baseName}} 导出成功: 🗂️ {{name}}" + }, + "failed": { + "message": "❌ {{baseName}} 导出失败: {{errorMessage}}" + } + }, + "task": { + "ai": { + "failed": { + "title": "AI 任务失败,表 {{tableName}}", + "message": "AI 任务在表 {{tableName}} 的字段 {{fieldName}} 中失败。\n\n{{errorMsg}}\n\n点击下面的按钮查看详细信息。" + } + } + } + } + } }, "waitlist": { "title": "等待列表", diff --git a/packages/core/src/errors/http/http-response.types.ts b/packages/core/src/errors/http/http-response.types.ts index a75fc7ad92..a043a0c61e 100644 --- a/packages/core/src/errors/http/http-response.types.ts +++ b/packages/core/src/errors/http/http-response.types.ts @@ -59,6 +59,6 @@ export enum HttpErrorCode { AUTOMATION_NODE_TEST_OUTDATED = 'automation_node_test_outdated', } -export type ICustomHttpExceptionData = Record & { - localization?: ILocalization; +export type ICustomHttpExceptionData = Record & { + localization?: ILocalization; }; diff --git a/packages/core/src/errors/types.ts b/packages/core/src/errors/types.ts index 3c6438378e..1c0a04b47b 100644 --- a/packages/core/src/errors/types.ts +++ b/packages/core/src/errors/types.ts @@ -5,4 +5,7 @@ export const localizationSchema = z.object({ context: z.record(z.unknown()).optional(), }); -export type ILocalization = z.infer; +export type ILocalization = { + i18nKey: T; + context?: Record; +}; diff --git a/packages/core/src/models/notification/notification.schema.ts b/packages/core/src/models/notification/notification.schema.ts index 42267e1b00..49c5d5796f 100644 --- a/packages/core/src/models/notification/notification.schema.ts +++ b/packages/core/src/models/notification/notification.schema.ts @@ -34,6 +34,7 @@ export const notificationSchema = z.object({ notifyType: z.nativeEnum(NotificationTypeEnum), url: z.string(), message: z.string(), + messageI18n: z.string().nullable().optional(), isRead: z.boolean(), createdTime: z.string(), }); diff --git a/packages/db-main-prisma/prisma/postgres/migrations/20251028105638_add_user_lang/migration.sql b/packages/db-main-prisma/prisma/postgres/migrations/20251028105638_add_user_lang/migration.sql new file mode 100644 index 0000000000..7abf40ef51 --- /dev/null +++ b/packages/db-main-prisma/prisma/postgres/migrations/20251028105638_add_user_lang/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "lang" TEXT; diff --git a/packages/db-main-prisma/prisma/postgres/migrations/20251029141643_add_notification_message_i18n/migration.sql b/packages/db-main-prisma/prisma/postgres/migrations/20251029141643_add_notification_message_i18n/migration.sql new file mode 100644 index 0000000000..fff53b8d2d --- /dev/null +++ b/packages/db-main-prisma/prisma/postgres/migrations/20251029141643_add_notification_message_i18n/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "notification" ADD COLUMN "message_i18n" TEXT; diff --git a/packages/db-main-prisma/prisma/postgres/schema.prisma b/packages/db-main-prisma/prisma/postgres/schema.prisma index 6e6e32278e..6c230871a6 100644 --- a/packages/db-main-prisma/prisma/postgres/schema.prisma +++ b/packages/db-main-prisma/prisma/postgres/schema.prisma @@ -185,6 +185,7 @@ model User { isSystem Boolean? @map("is_system") isAdmin Boolean? @map("is_admin") isTrialUsed Boolean? @map("is_trial_used") + lang String? @map("lang") notifyMeta String? @map("notify_meta") lastSignTime DateTime? @map("last_sign_time") deactivatedTime DateTime? @map("deactivated_time") @@ -309,6 +310,7 @@ model Notification { toUserId String @map("to_user_id") type String @map("type") message String @map("message") + messageI18n String? @map("message_i18n") urlPath String? @map("url_path") isRead Boolean @default(false) @map("is_read") createdTime DateTime @default(now()) @map("created_time") diff --git a/packages/db-main-prisma/prisma/sqlite/migrations/20251028105630_add_user_lang/migration.sql b/packages/db-main-prisma/prisma/sqlite/migrations/20251028105630_add_user_lang/migration.sql new file mode 100644 index 0000000000..b606398d78 --- /dev/null +++ b/packages/db-main-prisma/prisma/sqlite/migrations/20251028105630_add_user_lang/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "lang" TEXT; diff --git a/packages/db-main-prisma/prisma/sqlite/migrations/20251029141619_add_notification_message_i18n/migration.sql b/packages/db-main-prisma/prisma/sqlite/migrations/20251029141619_add_notification_message_i18n/migration.sql new file mode 100644 index 0000000000..442fd1fc45 --- /dev/null +++ b/packages/db-main-prisma/prisma/sqlite/migrations/20251029141619_add_notification_message_i18n/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "notification" ADD COLUMN "message_i18n" TEXT; diff --git a/packages/db-main-prisma/prisma/sqlite/schema.prisma b/packages/db-main-prisma/prisma/sqlite/schema.prisma index 089de3db6f..f78fd323a6 100644 --- a/packages/db-main-prisma/prisma/sqlite/schema.prisma +++ b/packages/db-main-prisma/prisma/sqlite/schema.prisma @@ -185,6 +185,7 @@ model User { isSystem Boolean? @map("is_system") isAdmin Boolean? @map("is_admin") isTrialUsed Boolean? @map("is_trial_used") + lang String? @map("lang") notifyMeta String? @map("notify_meta") lastSignTime DateTime? @map("last_sign_time") deactivatedTime DateTime? @map("deactivated_time") @@ -309,6 +310,7 @@ model Notification { toUserId String @map("to_user_id") type String @map("type") message String @map("message") + messageI18n String? @map("message_i18n") urlPath String? @map("url_path") isRead Boolean @default(false) @map("is_read") createdTime DateTime @default(now()) @map("created_time") diff --git a/packages/db-main-prisma/prisma/template.prisma b/packages/db-main-prisma/prisma/template.prisma index 658c6e3745..73d32d4cc5 100644 --- a/packages/db-main-prisma/prisma/template.prisma +++ b/packages/db-main-prisma/prisma/template.prisma @@ -185,6 +185,7 @@ model User { isSystem Boolean? @map("is_system") isAdmin Boolean? @map("is_admin") isTrialUsed Boolean? @map("is_trial_used") + lang String? @map("lang") notifyMeta String? @map("notify_meta") lastSignTime DateTime? @map("last_sign_time") deactivatedTime DateTime? @map("deactivated_time") @@ -309,6 +310,7 @@ model Notification { toUserId String @map("to_user_id") type String @map("type") message String @map("message") + messageI18n String? @map("message_i18n") urlPath String? @map("url_path") isRead Boolean @default(false) @map("is_read") createdTime DateTime @default(now()) @map("created_time") diff --git a/packages/openapi/src/auth/user-me.ts b/packages/openapi/src/auth/user-me.ts index b117601e14..dc02d5aa3e 100644 --- a/packages/openapi/src/auth/user-me.ts +++ b/packages/openapi/src/auth/user-me.ts @@ -15,6 +15,7 @@ export const userMeVoSchema = z.object({ notifyMeta: userNotifyMetaSchema, hasPassword: z.boolean(), isAdmin: z.boolean().nullable().optional(), + lang: z.string().nullable().optional(), organization: z .object({ id: z.string(), diff --git a/packages/openapi/src/mail/types.ts b/packages/openapi/src/mail/types.ts index 35cc63002b..e705062175 100644 --- a/packages/openapi/src/mail/types.ts +++ b/packages/openapi/src/mail/types.ts @@ -23,6 +23,12 @@ export enum MailType { AutomationSendEmailAction = 'automationSendEmailAction', } +export enum EmailVerifyCodeType { + Signup = 'signup', + ChangeEmail = 'changeEmail', + DomainVerification = 'domainVerification', +} + export const mailTransportConfigSchema = z.object({ senderName: z.string().optional(), sender: z.string(), diff --git a/packages/openapi/src/user/index.ts b/packages/openapi/src/user/index.ts index 6337b36bc7..2728146c51 100644 --- a/packages/openapi/src/user/index.ts +++ b/packages/openapi/src/user/index.ts @@ -1,4 +1,5 @@ export * from './update-name'; export * from './update-avatar'; export * from './update-notify-meta'; +export * from './update-lang'; export * from './last-visit'; diff --git a/packages/openapi/src/user/update-lang.ts b/packages/openapi/src/user/update-lang.ts new file mode 100644 index 0000000000..4e839049a5 --- /dev/null +++ b/packages/openapi/src/user/update-lang.ts @@ -0,0 +1,37 @@ +import type { RouteConfig } from '@asteasolutions/zod-to-openapi'; +import { axios } from '../axios'; +import { registerRoute } from '../utils'; +import { z } from '../zod'; + +export const UPDATE_USER_LANG = '/user/lang'; + +export const updateUserLangRoSchema = z.object({ + lang: z.string(), +}); + +export type IUpdateUserLangRo = z.infer; + +export const UpdateUserLangRoute: RouteConfig = registerRoute({ + method: 'patch', + path: UPDATE_USER_LANG, + description: 'Update user language', + request: { + body: { + content: { + 'application/json': { + schema: updateUserLangRoSchema, + }, + }, + }, + }, + responses: { + 200: { + description: 'Successfully update.', + }, + }, + tags: ['user'], +}); + +export const updateUserLang = async (updateUserLangRo: IUpdateUserLangRo) => { + return axios.patch(UPDATE_USER_LANG, updateUserLangRo); +}; diff --git a/packages/sdk/src/context/session/SessionProvider.tsx b/packages/sdk/src/context/session/SessionProvider.tsx index 4d4388ad9b..3c47c7193b 100644 --- a/packages/sdk/src/context/session/SessionProvider.tsx +++ b/packages/sdk/src/context/session/SessionProvider.tsx @@ -1,6 +1,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; -import { userMe } from '@teable/openapi'; +import { updateUserLang, userMe } from '@teable/openapi'; import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from '../app/i18n'; import type { IUser } from './SessionContext'; import { SessionContext } from './SessionContext'; @@ -14,6 +15,7 @@ export const SessionProvider: React.FC { const { user, fallback, children, disabledApi = false } = props; + const { lang } = useTranslation(); const queryClient = useQueryClient(); const [currentUser, setCurrentUser] = useState(() => { if (user) { @@ -22,10 +24,19 @@ export const SessionProvider: React.FC updateUserLang(ro), + }); + const { data: userQuery } = useQuery({ queryKey: ['user-me'], queryFn: () => userMe().then((res) => res.data), enabled: !disabledApi, + onSuccess: (data) => { + if (!data.lang && lang) { + updateLang({ lang }); + } + }, }); const { mutateAsync: getUser } = useMutation({ mutationFn: userMe }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb2f04a3b2..ee999075c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -315,6 +315,9 @@ importers: nestjs-cls: specifier: 4.3.0 version: 4.3.0(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(@nestjs/core@10.3.5)(reflect-metadata@0.2.1)(rxjs@7.8.1) + nestjs-i18n: + specifier: 10.5.1 + version: 10.5.1(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(@nestjs/core@10.3.5)(class-validator@0.14.1)(rxjs@7.8.1) nestjs-pino: specifier: 4.4.1 version: 4.4.1(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(pino-http@10.5.0)(pino@9.14.0)(rxjs@7.8.1) @@ -8409,6 +8412,7 @@ packages: '@univerjs/core@0.3.0': resolution: {integrity: sha512-eUOpkFxsMT95fk+3wNDxU6g8g0XTE0waiCngrswnPy2YO0ezkXUF9dzkrT5mAiDC/k8KP/nCMr/U+iHTpThFLA==} peerDependencies: + '@univerjs/core': 0.3.0 react: ^16.9.0 || ^17.0.0 || ^18.0.0 rxjs: '>=7.0.0' @@ -8660,6 +8664,7 @@ packages: '@univerjs/core': 0.3.0 '@univerjs/engine-formula': 0.3.0 '@univerjs/sheets': 0.3.0 + '@univerjs/sheets-hyper-link': 0.3.0 rxjs: '>=7.0.0' '@univerjs/sheets-numfmt@0.3.0': @@ -8677,6 +8682,7 @@ packages: '@univerjs/sheets-thread-comment-base@0.3.0': resolution: {integrity: sha512-JNAMb52Nqf6KY5Usa3tTqilM0lz76xXEFnkDCOqt6Zv7rbLOpgKw1j9MriEov9lCQVFSnmTmabTOdR7iOXzW6w==} + deprecated: Package no longer supported. peerDependencies: '@univerjs/core': 0.3.0 '@univerjs/engine-formula': 0.3.0 @@ -8722,6 +8728,7 @@ packages: '@univerjs/engine-formula': 0.3.0 '@univerjs/engine-numfmt': 0.3.0 '@univerjs/rpc': 0.3.0 + '@univerjs/sheets': 0.3.0 rxjs: '>=7.0.0' '@univerjs/telemetry@0.3.0': @@ -8756,6 +8763,7 @@ packages: '@univerjs/core': 0.3.0 '@univerjs/design': 0.3.0 '@univerjs/engine-render': 0.3.0 + '@univerjs/ui': 0.3.0 clsx: '>=2.0.0' react: ^16.9.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.9.0 || ^17.0.0 || ^18.0.0 @@ -8983,6 +8991,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + accept-language-parser@1.5.0: + resolution: {integrity: sha512-QhyTbMLYo0BBGg1aWbeMG4ekWtds/31BrEU+DONOg/7ax23vxpL03Pb7/zBmha2v7vdD3AyzZVWBVGEZxKOXWw==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -14087,6 +14098,15 @@ packages: reflect-metadata: '*' rxjs: '>= 7' + nestjs-i18n@10.5.1: + resolution: {integrity: sha512-cJJFz+RUfav23QACpGCq1pdXNLYC3tBesrP14RGoE/YYcD4xosQPX2eyjvDNuo0Ti63Xtn6j57wDNEUKrZqmSw==} + engines: {node: '>=18'} + peerDependencies: + '@nestjs/common': '*' + '@nestjs/core': '*' + class-validator: '*' + rxjs: '*' + nestjs-pino@4.4.1: resolution: {integrity: sha512-/E/JOtsUf/yHFGJx+zxBfwjCn1uJVV9AxSyx/mD0oNFCP+psvv9XE7WGh5Cebo6TwukF4qEu37eissGErVwLVg==} engines: {node: '>= 14'} @@ -17010,6 +17030,9 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-format@2.0.0: + resolution: {integrity: sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA==} + string-hash@1.1.3: resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==} @@ -26370,6 +26393,7 @@ snapshots: '@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1)': dependencies: + '@univerjs/core': 0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1) '@univerjs/protocol': 0.1.39-alpha.15(@grpc/grpc-js@1.12.4)(rxjs@7.8.1) '@wendellhu/redi': 0.16.1 lodash-es: 4.17.21 @@ -26697,6 +26721,7 @@ snapshots: '@univerjs/engine-formula': 0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1) '@univerjs/protocol': 0.1.39-alpha.15(@grpc/grpc-js@1.12.4)(rxjs@7.8.1) '@univerjs/sheets': 0.3.0(@grpc/grpc-js@1.12.4)(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/engine-formula@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1))(@univerjs/engine-numfmt@0.3.0)(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1) + '@univerjs/sheets-hyper-link': 0.3.0(@grpc/grpc-js@1.12.4)(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/engine-formula@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1))(@univerjs/sheets@0.3.0(@grpc/grpc-js@1.12.4)(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/engine-formula@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1))(@univerjs/engine-numfmt@0.3.0)(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1) rxjs: 7.8.1 transitivePeerDependencies: - '@grpc/grpc-js' @@ -26789,6 +26814,7 @@ snapshots: '@univerjs/engine-numfmt': 0.3.0 '@univerjs/protocol': 0.1.39-alpha.15(@grpc/grpc-js@1.12.4)(rxjs@7.8.1) '@univerjs/rpc': 0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1) + '@univerjs/sheets': 0.3.0(@grpc/grpc-js@1.12.4)(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/engine-formula@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1))(@univerjs/engine-numfmt@0.3.0)(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1) rxjs: 7.8.1 transitivePeerDependencies: - '@grpc/grpc-js' @@ -26831,6 +26857,7 @@ snapshots: '@univerjs/engine-formula': 0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(rxjs@7.8.1) '@univerjs/engine-render': 0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1) '@univerjs/icons': 0.1.87(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@univerjs/ui': 0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(@univerjs/design@0.3.0(clsx@2.1.1)(date-fns@4.1.0)(dayjs@1.11.10)(luxon@3.5.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@univerjs/engine-render@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(@univerjs/rpc@0.3.0(@univerjs/core@0.3.0(@grpc/grpc-js@1.12.4)(react@18.3.1)(rxjs@7.8.1))(rxjs@7.8.1))(clsx@2.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rxjs@7.8.1)(typescript@5.4.3) clsx: 2.1.1 localforage: 1.10.0 rc-notification: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -27185,6 +27212,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + accept-language-parser@1.5.0: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -33624,6 +33653,19 @@ snapshots: reflect-metadata: 0.2.1 rxjs: 7.8.1 + nestjs-i18n@10.5.1(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(@nestjs/core@10.3.5)(class-validator@0.14.1)(rxjs@7.8.1): + dependencies: + '@nestjs/common': 10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) + '@nestjs/core': 10.3.5(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(@nestjs/platform-express@10.3.5)(@nestjs/websockets@10.3.5)(encoding@0.1.13)(reflect-metadata@0.2.1)(rxjs@7.8.1) + accept-language-parser: 1.5.0 + chokidar: 3.6.0 + class-validator: 0.14.1 + cookie: 0.7.1 + iterare: 1.2.1 + js-yaml: 4.1.0 + rxjs: 7.8.1 + string-format: 2.0.0 + nestjs-pino@4.4.1(@nestjs/common@10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1))(pino-http@10.5.0)(pino@9.14.0)(rxjs@7.8.1): dependencies: '@nestjs/common': 10.3.5(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.1)(rxjs@7.8.1) @@ -37128,6 +37170,8 @@ snapshots: string-argv@0.3.2: {} + string-format@2.0.0: {} + string-hash@1.1.3: {} string-width@4.2.3: