Merge pull request #42 from Hexastack/fix/smtp-config

fix: smtp config
This commit is contained in:
Mohamed Marrouchi 2024-09-19 06:35:25 +01:00 committed by GitHub
commit ced9f0538c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 157 additions and 213 deletions

View File

@ -9,7 +9,10 @@ define add_service
COMPOSE_FILES += -f ./docker/docker-compose.$(1).prod.yml COMPOSE_FILES += -f ./docker/docker-compose.$(1).prod.yml
endif endif
else else
COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml -f ./docker/docker-compose.$(1).dev.yml COMPOSE_FILES += -f ./docker/docker-compose.$(1).yml
ifneq ($(wildcard ./docker/docker-compose.$(1).dev.yml),)
COMPOSE_FILES += -f ./docker/docker-compose.$(1).dev.yml
endif
endif endif
endef endef
@ -23,6 +26,10 @@ ifneq ($(NLU),)
$(eval $(call add_service,nlu)) $(eval $(call add_service,nlu))
endif endif
ifneq ($(SMTP4DEV),)
$(eval $(call add_service,smtp4dev))
endif
# Ensure .env file exists and matches .env.example # Ensure .env file exists and matches .env.example
check-env: check-env:
@if [ ! -f "./docker/.env" ]; then \ @if [ ! -f "./docker/.env" ]; then \
@ -48,4 +55,4 @@ destroy: check-env
docker compose $(COMPOSE_FILES) down -v docker compose $(COMPOSE_FILES) down -v
migrate-up: migrate-up:
docker-compose $(COMPOSE_FILES) up --no-deps -d database-init docker-compose $(COMPOSE_FILES) up --no-deps -d database-init

View File

@ -57,24 +57,28 @@ const i18nOptions: I18nOptions = {
@Module({ @Module({
imports: [ imports: [
MailerModule.forRoot({ ...(config.emails.isEnabled
transport: new SMTPTransport({ ? [
...config.emails.smtp, MailerModule.forRoot({
logger: true, transport: new SMTPTransport({
}), ...config.emails.smtp,
template: { logger: true,
adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }), debug: false,
dir: './src/templates', }),
options: { template: {
context: { adapter: new MjmlAdapter('ejs', { inlineCssEnabled: false }),
appName: config.parameters.appName, dir: './src/templates',
appUrl: config.parameters.appUrl, options: {
// TODO: add i18n support context: {
}, appName: config.parameters.appName,
}, appUrl: config.parameters.appUrl,
}, },
defaults: { from: config.parameters.email.main }, },
}), },
defaults: { from: config.emails.from },
}),
]
: []),
MongooseModule.forRoot(config.mongo.uri, { MongooseModule.forRoot(config.mongo.uri, {
dbName: config.mongo.dbName, dbName: config.mongo.dbName,
connectionFactory: (connection) => { connectionFactory: (connection) => {

View File

@ -8,19 +8,14 @@
*/ */
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ExtendedI18nService } from './extended-i18n.service'; import { ExtendedI18nService } from './extended-i18n.service';
@Injectable() @Injectable()
export class AppService { export class AppService {
constructor( constructor(private readonly i18n: ExtendedI18nService) {}
private readonly i18n: ExtendedI18nService,
private readonly eventEmitter: EventEmitter2,
) {}
getHello(): string { getHello(): string {
this.eventEmitter.emit('hook:i18n:refresh', []); return this.i18n.t('welcome', { lang: 'en' });
return this.i18n.t('Welcome');
} }
} }

View File

@ -1,18 +1,21 @@
{ {
"invitation_subject": "[Hexabot] Sign-Up Invitation",
"account_confirmation_subject": "[Hexabot] Account Confirmation",
"password_reset_subject": "[Hexabot] Password Reset",
"welcome": "Welcome", "welcome": "Welcome",
"hi": "Hi", "hi": "Hi",
"invitation_to_join": "Invitation to join", "invitation_to_join": "Invitation to join",
"invitation_for_account_creation": "You have been invited to create a", "invitation_for_account_creation": "You have been invited to create a",
"account": "account", "account": "account",
"create_account:": "Click on the button below to create your account:", "create_account": "Click on the button below to create your account:",
"registration_failed": "Registration failed! Either email address or invitation token is invalid.", "registration_failed": "Registration failed! Either email address or invitation token is invalid.",
"create_account_button": "Click on the button below to create your account", "create_account_button": "Click on the button below to create your account",
"join": "Join", "join": "Join",
"account_successfully_created_confirm_password": "You have successfully created an account but you will need to confirm your email address.", "account_successfully_created_confirm_password": "You have successfully created an account but you will need to confirm your email address.",
"confirm_account:": "Click on the button below in order to confirm your account:", "confirm_account": "Click on the button below in order to confirm your account:",
"password_reset_request": "A request to reset the password for your account has been made.", "password_reset_request": "A request to reset the password for your account has been made.",
"reset": "Reset", "reset": "Reset",
"click_to_reset_password:": "Click on the button below in order to set a new password:", "click_to_reset_password": "Click on the button below in order to set a new password:",
"confirm": "Confirm", "confirm": "Confirm",
"best_regards": "Best Regards," "best_regards": "Best Regards,"
} }

View File

@ -1,4 +1,7 @@
{ {
"invitation_subject": "[Hexabot] Invitation à s'inscrire",
"account_confirmation_subject": "[Hexabot] Confirmation de compte",
"password_reset_subject": "[Hexabot] Réinitialisation du mot de passe",
"welcome": "Bienvenue", "welcome": "Bienvenue",
"hi": "Bonjour", "hi": "Bonjour",
"invitation_to_join": "Invitation", "invitation_to_join": "Invitation",

View File

@ -13,7 +13,7 @@ import { Config } from './types';
export const config: Config = { export const config: Config = {
i18n: { i18n: {
translationFilename: process.env.I18N_TRANSLATION_FILENAME || '', translationFilename: process.env.I18N_TRANSLATION_FILENAME || 'messages',
}, },
appPath: process.cwd(), appPath: process.cwd(),
apiPath: process.env.API_ORIGIN, apiPath: process.env.API_ORIGIN,
@ -77,7 +77,7 @@ export const config: Config = {
: [undefined], // ['http://example.com', 'https://example.com'], : [undefined], // ['http://example.com', 'https://example.com'],
}, },
session: { session: {
secret: process.env.SESSION_SECRET || '4fac3596aeb0d048e7b6b38235c29248', secret: process.env.SESSION_SECRET || 'changeme',
name: process.env.SESSION_NAME || 'hex.sid', name: process.env.SESSION_NAME || 'hex.sid',
adapter: 'connect-mongo', adapter: 'connect-mongo',
url: 'mongodb://localhost:27017/hexabot', url: 'mongodb://localhost:27017/hexabot',
@ -90,23 +90,18 @@ export const config: Config = {
}, },
}, },
emails: { emails: {
isEnabled: process.env.EMAIL_SMTP_ENABLED === 'true' || false,
smtp: { smtp: {
port: parseInt(process.env.EMAIL_SMTP_PORT) || 25, port: parseInt(process.env.EMAIL_SMTP_PORT) || 25,
host: process.env.EMAIL_SMTP_HOST || 'smtp.mailgun.org', host: process.env.EMAIL_SMTP_HOST || 'localhost',
ignoreTLS: false,
secure: process.env.EMAIL_SMTP_SECURE === 'true' || false, secure: process.env.EMAIL_SMTP_SECURE === 'true' || false,
auth: { auth: {
user: user: process.env.EMAIL_SMTP_USER || '',
process.env.EMAIL_SMTP_USER || pass: process.env.EMAIL_SMTP_PASS || '',
'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
pass: process.env.EMAIL_SMTP_PASS || 'e58526b30ad640394b5c77a211a19c5b',
}, },
}, },
}, from: process.env.EMAIL_SMTP_FROM || 'noreply@example.com',
datastores: {
default: {
adapter: 'sails-mongo',
url: 'mongodb://localhost:27017/hexabot',
},
}, },
parameters: { parameters: {
uploadDir: uploadDir:
@ -117,17 +112,9 @@ export const config: Config = {
maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES maxUploadSize: process.env.UPLOAD_MAX_SIZE_IN_BYTES
? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES) ? Number(process.env.UPLOAD_MAX_SIZE_IN_BYTES)
: 2000000, : 2000000,
transport: 'smtp',
email: {
main: 'postmaster@sandbox9471202ff10448c7ac917618fe94d8e1.mailgun.org',
},
appName: 'Hexabot.ai', appName: 'Hexabot.ai',
apiUrl: 'http://localhost:4000', apiUrl: 'http://localhost:4000',
appUrl: 'http://localhost:8081', appUrl: 'http://localhost:8081',
geocoder: {
provider: 'opencage',
apiKey: 'c2a490d593b14612aefa6ec2e6b77c47',
},
}, },
pagination: { pagination: {
limit: 10, limit: 10,
@ -135,7 +122,7 @@ export const config: Config = {
chatbot: { chatbot: {
lang: { lang: {
default: 'en', default: 'en',
available: ['en', 'fr', 'ar', 'tn'], available: ['en', 'fr'],
}, },
messages: { messages: {
track_delivery: false, track_delivery: false,

View File

@ -7,6 +7,7 @@
* 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited. * 3. SaaS Restriction: This software, or any derivative of it, may not be used to offer a competing product or service (SaaS) without prior written consent from Hexastack. Offering the software as a service or using it in a commercial cloud environment without express permission is strictly prohibited.
*/ */
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import type { ServerOptions, Socket } from 'socket.io'; import type { ServerOptions, Socket } from 'socket.io';
type TJwtOptions = { type TJwtOptions = {
@ -69,38 +70,18 @@ export type Config = {
}; };
}; };
emails: { emails: {
smtp: { isEnabled: boolean;
port: number; smtp: Partial<SMTPConnection.Options>;
host: string; from: string;
secure: boolean;
auth: {
user: string;
pass: string;
};
};
};
datastores: {
default: {
adapter: string;
url: string;
};
}; };
parameters: { parameters: {
uploadDir: string; uploadDir: string;
avatarDir: string; avatarDir: string;
storageMode: 'disk' | 'memory'; storageMode: 'disk' | 'memory';
maxUploadSize: number; maxUploadSize: number;
transport: string;
email: {
main: string;
};
appName: string; appName: string;
apiUrl: string; apiUrl: string;
appUrl: string; appUrl: string;
geocoder: {
provider: string;
apiKey: string;
};
}; };
pagination: { pagination: {
limit: number; limit: number;

View File

@ -58,9 +58,11 @@ export class ExtendedI18nService<
initDynamicTranslations(translations: Translation[]) { initDynamicTranslations(translations: Translation[]) {
this.dynamicTranslations = translations.reduce((acc, curr) => { this.dynamicTranslations = translations.reduce((acc, curr) => {
const { str, translations } = curr; const { str, translations } = curr;
Object.entries(translations).forEach(([lang, t]) => { Object.entries(translations)
acc[lang][str] = t; .filter(([lang]) => lang in acc)
}); .forEach(([lang, t]) => {
acc[lang][str] = t;
});
return acc; return acc;
}, this.dynamicTranslations); }, this.dynamicTranslations);

View File

@ -111,10 +111,4 @@ export type Settings = {
fallback_message: string[]; fallback_message: string[];
fallback_block: string; fallback_block: string;
}; };
email_settings: {
mailer: string;
auth_user: string;
auth_pass: string;
from: string;
};
} & Record<string, any>; } & Record<string, any>;

View File

@ -99,56 +99,6 @@ export const settingModels: SettingCreateDto[] = [
}, },
weight: 6, weight: 6,
}, },
{
group: 'email_settings',
label: 'from',
value: 'no-reply@domain.com',
type: SettingType.text,
weight: 1,
},
{
group: 'email_settings',
label: 'mailer',
value: 'sendmail',
options: ['sendmail', 'smtp'],
type: SettingType.select,
weight: 2,
},
{
group: 'email_settings',
label: 'host',
value: 'localhost',
type: SettingType.text,
weight: 3,
},
{
group: 'email_settings',
label: 'port',
value: '25',
type: SettingType.text,
weight: 4,
},
{
group: 'email_settings',
label: 'secure',
value: true,
type: SettingType.checkbox,
weight: 5,
},
{
group: 'email_settings',
label: 'auth_user',
value: '',
type: SettingType.text,
weight: 6,
},
{
group: 'email_settings',
label: 'auth_pass',
value: '',
type: SettingType.text,
weight: 7,
},
{ {
group: 'contact', group: 'contact',
label: 'contact_email_recipient', label: 'contact_email_recipient',

View File

@ -24,10 +24,10 @@
</mj-section> </mj-section>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %>,</mj-text ><%= t('best_regards') %></mj-text
> >
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text ><%= this.appName %></mj-text
> >
</mj-column> </mj-column>

View File

@ -26,11 +26,11 @@
</mj-section> </mj-section>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %></mj-text ><%= t('best_regards') %></mj-text
> >
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text ><%= this.appName %></mj-text
> >
</mj-column> </mj-column>

View File

@ -23,10 +23,10 @@
</mj-section> </mj-section>
<mj-section> <mj-section>
<mj-column> <mj-column>
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= t('best_regards') %>,</mj-text ><%= t('best_regards') %></mj-text
> >
<mj-text font-size="14px" color="#000" font-family="helvetica" <mj-text font-size="16px" color="#000" font-family="helvetica"
><%= this.appName %></mj-text ><%= this.appName %></mj-text
> >
</mj-column> </mj-column>

View File

@ -11,6 +11,7 @@ import {
Inject, Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
Optional,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { MailerService } from '@nestjs-modules/mailer'; import { MailerService } from '@nestjs-modules/mailer';
@ -32,7 +33,7 @@ export class InvitationService extends BaseService<Invitation> {
@Inject(InvitationRepository) @Inject(InvitationRepository)
readonly repository: InvitationRepository, readonly repository: InvitationRepository,
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
private readonly mailerService: MailerService, @Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService, private logger: LoggerService,
protected readonly i18n: ExtendedI18nService, protected readonly i18n: ExtendedI18nService,
) { ) {
@ -54,24 +55,28 @@ export class InvitationService extends BaseService<Invitation> {
*/ */
async create(dto: InvitationCreateDto): Promise<Invitation> { async create(dto: InvitationCreateDto): Promise<Invitation> {
const jwt = await this.sign(dto); const jwt = await this.sign(dto);
try { if (this.mailerService) {
await this.mailerService.sendMail({ try {
to: dto.email, await this.mailerService.sendMail({
template: 'invitation.mjml', to: dto.email,
context: { template: 'invitation.mjml',
token: jwt, context: {
// TODO: Which language should we use? token: jwt,
t: (key: string) => this.i18n.t(key), // TODO: Which language should we use?
}, t: (key: string) =>
}); this.i18n.t(key, { lang: config.chatbot.lang.default }),
} catch (e) { },
this.logger.error( subject: this.i18n.t('invitation_subject'),
'Could not send email', });
e.message, } catch (e) {
e.stack, this.logger.error(
'InvitationService', 'Could not send email',
); e.message,
throw new InternalServerErrorException('Could not send email'); e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
}
} }
const newInvitation = await super.create({ ...dto, token: jwt }); const newInvitation = await super.create({ ...dto, token: jwt });
return { ...newInvitation, token: jwt }; return { ...newInvitation, token: jwt };

View File

@ -13,6 +13,7 @@ import {
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
NotFoundException, NotFoundException,
Optional,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { JwtService, JwtSignOptions } from '@nestjs/jwt';
@ -30,7 +31,7 @@ import { UserRequestResetDto, UserResetPasswordDto } from '../dto/user.dto';
export class PasswordResetService { export class PasswordResetService {
constructor( constructor(
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
private readonly mailerService: MailerService, @Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService, private logger: LoggerService,
private readonly userService: UserService, private readonly userService: UserService,
public readonly i18n: ExtendedI18nService, public readonly i18n: ExtendedI18nService,
@ -55,24 +56,29 @@ export class PasswordResetService {
throw new NotFoundException('User not found'); throw new NotFoundException('User not found');
} }
const jwt = await this.sign(dto); const jwt = await this.sign(dto);
try {
await this.mailerService.sendMail({ if (this.mailerService) {
to: dto.email, try {
template: 'password_reset.mjml', await this.mailerService.sendMail({
context: { to: dto.email,
token: jwt, template: 'password_reset.mjml',
first_name: user.first_name, context: {
t: (key: string) => this.i18n.t(key), token: jwt,
}, first_name: user.first_name,
}); t: (key: string) =>
} catch (e) { this.i18n.t(key, { lang: config.chatbot.lang.default }),
this.logger.error( },
'Could not send email', subject: this.i18n.t('password_reset_subject'),
e.message, });
e.stack, } catch (e) {
'InvitationService', this.logger.error(
); 'Could not send email',
throw new InternalServerErrorException('Could not send email'); e.message,
e.stack,
'InvitationService',
);
throw new InternalServerErrorException('Could not send email');
}
} }
// TODO: hash the token before saving it // TODO: hash the token before saving it

View File

@ -11,6 +11,7 @@ import {
Inject, Inject,
Injectable, Injectable,
InternalServerErrorException, InternalServerErrorException,
Optional,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { JwtService, JwtSignOptions } from '@nestjs/jwt'; import { JwtService, JwtSignOptions } from '@nestjs/jwt';
@ -33,7 +34,7 @@ export class ValidateAccountService {
constructor( constructor(
@Inject(JwtService) private readonly jwtService: JwtService, @Inject(JwtService) private readonly jwtService: JwtService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly mailerService: MailerService, @Optional() private readonly mailerService: MailerService | undefined,
private readonly i18n: ExtendedI18nService, private readonly i18n: ExtendedI18nService,
) {} ) {}
@ -71,16 +72,19 @@ export class ValidateAccountService {
) { ) {
const confirmationToken = await this.sign({ email: dto.email }); const confirmationToken = await this.sign({ email: dto.email });
await this.mailerService.sendMail({ if (this.mailerService) {
to: dto.email, await this.mailerService.sendMail({
template: 'account_confirmation.mjml', to: dto.email,
context: { template: 'account_confirmation.mjml',
token: confirmationToken, context: {
first_name: dto.first_name, token: confirmationToken,
t: (key: string) => this.i18n.t(key), first_name: dto.first_name,
}, t: (key: string) =>
subject: 'Account confirmation Email', this.i18n.t(key, { lang: config.chatbot.lang.default }),
}); },
subject: this.i18n.t('account_confirmation_subject'),
});
}
} }
/** /**

View File

@ -30,13 +30,15 @@ MONGO_PASSWORD=dev_only
MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/ MONGO_URI=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@mongo:27017/
MONGO_DB=hexabot MONGO_DB=hexabot
# SMTP Config for local dev env # SMTP Config (for local dev env, use smtp4dev by doing `make start SMTP4DEV=1`)
APP_SMTP_4_DEV_PORT=9002 APP_SMTP_4_DEV_PORT=9002
EMAIL_SMTP_ENABLED=false
EMAIL_SMTP_HOST=smtp4dev EMAIL_SMTP_HOST=smtp4dev
EMAIL_SMTP_PORT=25 EMAIL_SMTP_PORT=25
EMAIL_SMTP_SECURE=false EMAIL_SMTP_SECURE=false
EMAIL_SMTP_USER=dev_only EMAIL_SMTP_USER=dev_only
EMAIL_SMTP_PASS=dev_only EMAIL_SMTP_PASS=dev_only
EMAIL_SMTP_FROM=noreply@example.com
# NLU Server # NLU Server
AUTH_TOKEN=token123 AUTH_TOKEN=token123

View File

@ -17,21 +17,6 @@ services:
#- ../api/node_modules:/app/node_modules #- ../api/node_modules:/app/node_modules
command: ["npm", "run", "start:debug"] command: ["npm", "run", "start:debug"]
smtp4dev:
image: rnwood/smtp4dev:v3
restart: always
ports:
- ${APP_SMTP_4_DEV_PORT}:80
- "25:25"
- "143:143"
volumes:
- smtp4dev-data:/smtp4dev
environment:
- ServerOptions__HostName=smtp4dev
- ServerOptions__LockSettings=true
networks:
- db-network
mongo-express: mongo-express:
container_name: mongoUi container_name: mongoUi
image: mongo-express:1-20 image: mongo-express:1-20
@ -52,6 +37,3 @@ services:
- ../widget/src:/app/src - ../widget/src:/app/src
ports: ports:
- ${APP_WIDGET_PORT}:5173 - ${APP_WIDGET_PORT}:5173
volumes:
smtp4dev-data:

View File

@ -0,0 +1,20 @@
version: "3.8"
services:
smtp4dev:
image: rnwood/smtp4dev:v3
restart: always
ports:
- ${APP_SMTP_4_DEV_PORT}:80
- "25:25"
- "143:143"
volumes:
- smtp4dev-data:/smtp4dev
environment:
- ServerOptions__HostName=smtp4dev
- ServerOptions__LockSettings=true
networks:
- app-network
volumes:
smtp4dev-data:

View File

@ -40,7 +40,7 @@
"edit_account_email": "A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.", "edit_account_email": "A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.",
"new_password": "To change the current password, enter the new password in both fields.", "new_password": "To change the current password, enter the new password in both fields.",
"account_update_success": "Account has been updated successfully", "account_update_success": "Account has been updated successfully",
"account_disabled": "Your account has been disabled!", "account_disabled": "Your account has been either disabled or is pending confirmation.",
"success_invitation_sent": "Invitation to join has been successfully sent.", "success_invitation_sent": "Invitation to join has been successfully sent.",
"item_delete_confirm": "Are you sure you want to delete this item?", "item_delete_confirm": "Are you sure you want to delete this item?",
"item_delete_success": "Item has been deleted successfully", "item_delete_success": "Item has been deleted successfully",
@ -213,7 +213,6 @@
"offline": "Web Channel", "offline": "Web Channel",
"twitter": "Twitter", "twitter": "Twitter",
"dimelo": "Dimelo", "dimelo": "Dimelo",
"email_settings": "Email",
"contact": "Contact Infos", "contact": "Contact Infos",
"chatbot_settings": "Chatbot", "chatbot_settings": "Chatbot",
"nlp_settings": "NLP Provider", "nlp_settings": "NLP Provider",

View File

@ -40,7 +40,7 @@
"edit_account_email": "Une adresse e-mail valide Tous les e-mails du système seront envoyés à cette adresse. L'adresse e-mail n'est pas rendue publique et ne sera utilisée que si vous souhaitez recevoir un nouveau mot de passe ou si vous souhaitez recevoir certaines nouvelles ou notifications par e-mail.", "edit_account_email": "Une adresse e-mail valide Tous les e-mails du système seront envoyés à cette adresse. L'adresse e-mail n'est pas rendue publique et ne sera utilisée que si vous souhaitez recevoir un nouveau mot de passe ou si vous souhaitez recevoir certaines nouvelles ou notifications par e-mail.",
"new_password": "Pour changer le mot de passe actuel, entrez le nouveau mot de passe dans les deux champs.", "new_password": "Pour changer le mot de passe actuel, entrez le nouveau mot de passe dans les deux champs.",
"account_update_success": "Le compte a été mis à jour avec succès", "account_update_success": "Le compte a été mis à jour avec succès",
"account_disabled": "Votre compte a été désactivé!", "account_disabled": "Votre compte a été désactivé ou est en attente de confirmation.",
"success_invitation_sent": "L'invitation a été envoyée avec succès.", "success_invitation_sent": "L'invitation a été envoyée avec succès.",
"item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?", "item_delete_confirm": "Êtes-vous sûr de bien vouloir supprimer cet élément?",
"item_delete_success": "L'élément a été supprimé avec succès", "item_delete_success": "L'élément a été supprimé avec succès",