feat: wrap up translation logic

This commit is contained in:
Mohamed Marrouchi
2024-09-24 11:23:40 +01:00
parent 16e7431d83
commit ecb8d9745a
35 changed files with 291 additions and 260 deletions

View File

@@ -44,7 +44,7 @@ import idPlugin from './utils/schema-plugin/id.plugin';
import { WebsocketModule } from './websocket/websocket.module';
const i18nOptions: I18nOptions = {
fallbackLanguage: config.chatbot.lang.default,
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/config/i18n/'),
watch: true,

View File

@@ -19,7 +19,10 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import { ContentRepository } from '@/cms/repositories/content.repository';
import { ContentModel } from '@/cms/schemas/content.schema';
import { ContentService } from '@/cms/services/content.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { SettingService } from '@/setting/services/setting.service';
@@ -86,6 +89,7 @@ describe('BlockController', () => {
UserModel,
RoleModel,
PermissionModel,
LanguageModel,
]),
],
providers: [
@@ -97,6 +101,7 @@ describe('BlockController', () => {
UserRepository,
RoleRepository,
PermissionRepository,
LanguageRepository,
BlockService,
LabelService,
CategoryService,
@@ -105,6 +110,7 @@ describe('BlockController', () => {
UserService,
RoleService,
PermissionService,
LanguageService,
PluginService,
LoggerService,
{

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.
*/
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { MongooseModule } from '@nestjs/mongoose';
import { Test } from '@nestjs/testing';
@@ -28,7 +29,10 @@ import OfflineHandler from '@/extensions/channels/offline/index.channel';
import { OFFLINE_CHANNEL_NAME } from '@/extensions/channels/offline/settings';
import { Offline } from '@/extensions/channels/offline/types';
import OfflineEventWrapper from '@/extensions/channels/offline/wrapper';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { PluginService } from '@/plugins/plugins.service';
import { Settings } from '@/setting/schemas/types';
@@ -92,6 +96,7 @@ describe('BlockService', () => {
ContentModel,
AttachmentModel,
LabelModel,
LanguageModel,
]),
],
providers: [
@@ -100,11 +105,13 @@ describe('BlockService', () => {
ContentTypeRepository,
ContentRepository,
AttachmentRepository,
LanguageRepository,
BlockService,
CategoryService,
ContentTypeService,
ContentService,
AttachmentService,
LanguageService,
{
provide: PluginService,
useValue: {},
@@ -130,6 +137,14 @@ describe('BlockService', () => {
},
},
EventEmitter2,
{
provide: CACHE_MANAGER,
useValue: {
del: jest.fn(),
get: jest.fn(),
set: jest.fn(),
},
},
],
}).compile();
blockService = module.get<BlockService>(BlockService);

View File

@@ -14,6 +14,7 @@ import { AttachmentService } from '@/attachment/services/attachment.service';
import EventWrapper from '@/channel/lib/EventWrapper';
import { ContentService } from '@/cms/services/content.service';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { Nlp } from '@/nlp/lib/types';
import { PluginService } from '@/plugins/plugins.service';
@@ -44,6 +45,7 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
private readonly pluginService: PluginService,
private readonly logger: LoggerService,
protected readonly i18n: I18nService,
protected readonly languageService: LanguageService,
) {
super(repository);
}
@@ -108,12 +110,9 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
// Check & catch user language through NLP
const nlp = event.getNLP();
if (nlp) {
const settings = await this.settingService.getSettings();
const languages = await this.languageService.getLanguages();
const lang = nlp.entities.find((e) => e.entity === 'language');
if (
lang &&
settings.nlp_settings.languages.indexOf(lang.value) !== -1
) {
if (lang && Object.keys(languages).indexOf(lang.value) !== -1) {
const profile = event.getSender();
profile.language = lang.value;
event.setSender(profile);
@@ -369,12 +368,11 @@ export class BlockService extends BaseService<Block, BlockPopulate, BlockFull> {
* @returns The text message translated and tokens being replaces with values
*/
processText(text: string, context: Context, settings: Settings): string {
const lang =
context && context.user && context.user.language
? context.user.language
: settings.nlp_settings.default_lang;
// Translate
text = this.i18n.t(text, { lang, defaultValue: text });
text = this.i18n.t(text, {
lang: context.user.language,
defaultValue: text,
});
// Replace context tokens
text = this.processTokenReplacements(text, context, settings);
return text;

View File

@@ -120,10 +120,6 @@ export const config: Config = {
limit: 10,
},
chatbot: {
lang: {
default: 'en',
available: ['en', 'fr'],
},
messages: {
track_delivery: false,
track_read: false,

View File

@@ -15,7 +15,6 @@ type TJwtOptions = {
secret: string;
expiresIn: string;
};
type TLanguage = 'en' | 'fr' | 'ar' | 'tn';
type TMethods = 'GET' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD';
type TLogLevel = 'log' | 'fatal' | 'error' | 'warn' | 'debug' | 'verbose';
type TCacheConfig = {
@@ -87,10 +86,6 @@ export type Config = {
limit: number;
};
chatbot: {
lang: {
default: TLanguage;
available: TLanguage[];
};
messages: {
track_delivery: boolean;
track_read: boolean;

View File

@@ -477,7 +477,7 @@ export default class OfflineHandler extends ChannelHandler {
...channelData,
name: this.getChannel(),
},
language: config.chatbot.lang.default,
language: '',
locale: '',
timezone: 0,
gender: 'male',

View File

@@ -87,8 +87,6 @@ describe('NLP Default Helper', () => {
provider: 'default',
endpoint: 'path',
token: 'token',
languages: ['fr', 'ar', 'tn'],
default_lang: 'fr',
threshold: '0.5',
},
})),

View File

@@ -51,8 +51,11 @@ import {
import { TranslationController } from './translation.controller';
import { TranslationUpdateDto } from '../dto/translation.dto';
import { LanguageRepository } from '../repositories/language.repository';
import { TranslationRepository } from '../repositories/translation.repository';
import { LanguageModel } from '../schemas/language.schema';
import { Translation, TranslationModel } from '../schemas/translation.schema';
import { LanguageService } from '../services/language.service';
import { TranslationService } from '../services/translation.service';
describe('TranslationController', () => {
@@ -73,6 +76,7 @@ describe('TranslationController', () => {
MenuModel,
BlockModel,
ContentModel,
LanguageModel,
]),
],
providers: [
@@ -117,7 +121,7 @@ describe('TranslationController', () => {
provide: I18nService,
useValue: {
t: jest.fn().mockImplementation((t) => t),
initDynamicTranslations: jest.fn(),
refreshDynamicTranslations: jest.fn(),
},
},
{
@@ -129,6 +133,8 @@ describe('TranslationController', () => {
},
},
LoggerService,
LanguageService,
LanguageRepository,
],
}).compile();
translationService = module.get<TranslationService>(TranslationService);

View File

@@ -14,9 +14,9 @@ import {
NotFoundException,
Param,
Patch,
Post,
Query,
UseInterceptors,
Post,
} from '@nestjs/common';
import { CsrfCheck } from '@tekuconcept/nestjs-csrf';
import { TFilterQuery } from 'mongoose';
@@ -31,12 +31,14 @@ import { SearchFilterPipe } from '@/utils/pipes/search-filter.pipe';
import { TranslationUpdateDto } from '../dto/translation.dto';
import { Translation } from '../schemas/translation.schema';
import { LanguageService } from '../services/language.service';
import { TranslationService } from '../services/translation.service';
@UseInterceptors(CsrfInterceptor)
@Controller('translation')
export class TranslationController extends BaseController<Translation> {
constructor(
private readonly languageService: LanguageService,
private readonly translationService: TranslationService,
private readonly settingService: SettingService,
private readonly logger: LoggerService,
@@ -103,40 +105,37 @@ export class TranslationController extends BaseController<Translation> {
@CsrfCheck(true)
@Post('refresh')
async refresh(): Promise<any> {
const settings = await this.settingService.getSettings();
const languages = settings.nlp_settings.languages;
const defaultTrans: Translation['translations'] = languages.reduce(
(acc, curr) => {
acc[curr] = '';
return acc;
},
{} as { [key: string]: string },
);
const defaultLanguage = await this.languageService.getDefaultLanguage();
const languages = await this.languageService.getLanguages();
const defaultTrans: Translation['translations'] = Object.keys(languages)
.filter((lang) => lang !== defaultLanguage.code)
.reduce(
(acc, curr) => {
acc[curr] = '';
return acc;
},
{} as { [key: string]: string },
);
// Scan Blocks
return this.translationService
.getAllBlockStrings()
.then(async (strings: string[]) => {
const settingStrings =
await this.translationService.getSettingStrings();
// Scan global settings
strings = strings.concat(settingStrings);
// Filter unique and not empty messages
strings = strings.filter((str, pos) => {
return str && strings.indexOf(str) == pos;
});
// Perform refresh
const queue = strings.map((str) =>
this.translationService.findOneOrCreate(
{ str },
{ str, translations: defaultTrans as any, translated: 100 },
),
);
return Promise.all(queue).then(() => {
// Purge non existing translations
return this.translationService.deleteMany({
str: { $nin: strings },
});
});
});
let strings = await this.translationService.getAllBlockStrings();
const settingStrings = await this.translationService.getSettingStrings();
// Scan global settings
strings = strings.concat(settingStrings);
// Filter unique and not empty messages
strings = strings.filter((str, pos) => {
return str && strings.indexOf(str) == pos;
});
// Perform refresh
const queue = strings.map((str) =>
this.translationService.findOneOrCreate(
{ str },
{ str, translations: defaultTrans },
),
);
await Promise.all(queue);
// Purge non existing translations
return this.translationService.deleteMany({
str: { $nin: strings },
});
}
}

View File

@@ -9,7 +9,7 @@
import { PartialType } from '@nestjs/mapped-types';
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class LanguageCreateDto {
@ApiProperty({ description: 'Language Title', type: String })
@@ -22,15 +22,14 @@ export class LanguageCreateDto {
@IsString()
code: string;
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
@IsNotEmpty()
@IsBoolean()
isDefault: boolean;
@ApiProperty({ description: 'Whether Language is RTL', type: Boolean })
@IsNotEmpty()
@IsBoolean()
isRTL?: boolean;
isRTL: boolean;
}
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {}
export class LanguageUpdateDto extends PartialType(LanguageCreateDto) {
@ApiProperty({ description: 'Is Default Language ?', type: Boolean })
@IsOptional()
@IsBoolean()
isDefault?: boolean;
}

View File

@@ -30,8 +30,9 @@ export class Language extends BaseSchema {
@Prop({
type: Boolean,
default: false,
})
isDefault: boolean;
isDefault?: boolean;
@Prop({
type: Boolean,

View File

@@ -11,6 +11,7 @@ import { Prop, Schema, SchemaFactory, ModelDefinition } from '@nestjs/mongoose';
import { THydratedDocument } from 'mongoose';
import { BaseSchema } from '@/utils/generics/base-schema';
import { LifecycleHookManager } from '@/utils/generics/lifecycle-hook-manager';
@Schema({ timestamps: true })
export class Translation extends BaseSchema {
@@ -26,17 +27,12 @@ export class Translation extends BaseSchema {
required: true,
})
translations: Record<string, string>;
@Prop({
type: Number,
})
translated: number;
}
export const TranslationModel: ModelDefinition = {
export const TranslationModel: ModelDefinition = LifecycleHookManager.attach({
name: Translation.name,
schema: SchemaFactory.createForClass(Translation),
};
});
export type TranslationDocument = THydratedDocument<Translation>;

View File

@@ -13,11 +13,11 @@ export const languageModels: LanguageCreateDto[] = [
{
title: 'English',
code: 'en',
isDefault: true,
isRTL: false,
},
{
title: 'Français',
code: 'fr',
isDefault: false,
isRTL: false,
},
];

View File

@@ -8,7 +8,6 @@
*/
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
I18nService as NativeI18nService,
Path,
@@ -24,11 +23,7 @@ import { Translation } from '@/i18n/schemas/translation.schema';
export class I18nService<
K = Record<string, unknown>,
> extends NativeI18nService<K> {
private dynamicTranslations: Record<string, Record<string, string>> =
config.chatbot.lang.available.reduce(
(acc, curr) => ({ ...acc, [curr]: {} }),
{},
);
private dynamicTranslations: Record<string, Record<string, string>> = {};
t<P extends Path<K> = any, R = PathValue<K, P>>(
key: P,
@@ -40,17 +35,19 @@ export class I18nService<
...options,
};
let { lang } = options;
lang = lang ?? this.i18nOptions.fallbackLanguage;
lang = this.resolveLanguage(lang);
// Translate block message, button text, ...
if (lang in this.dynamicTranslations) {
if (key in this.dynamicTranslations[lang]) {
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
R,
string,
R
>;
if (this.dynamicTranslations[lang][key]) {
return this.dynamicTranslations[lang][key] as IfAnyOrNever<
R,
string,
R
>;
}
return options.defaultValue as IfAnyOrNever<R, string, R>;
}
}
@@ -59,15 +56,13 @@ export class I18nService<
return super.t<P, R>(key, options);
}
@OnEvent('hook:i18n:refresh')
initDynamicTranslations(translations: Translation[]) {
refreshDynamicTranslations(translations: Translation[]) {
this.dynamicTranslations = translations.reduce((acc, curr) => {
const { str, translations } = curr;
Object.entries(translations)
.filter(([lang]) => lang in acc)
.forEach(([lang, t]) => {
acc[lang][str] = t;
});
Object.entries(translations).forEach(([lang, t]) => {
acc[lang] = acc[lang] || {};
acc[lang][str] = t;
});
return acc;
}, this.dynamicTranslations);

View File

@@ -33,7 +33,7 @@ export class TranslationService extends BaseService<Translation> {
public async resetI18nTranslations() {
const translations = await this.findAll();
this.i18n.initDynamicTranslations(translations);
this.i18n.refreshDynamicTranslations(translations);
}
/**

View File

@@ -12,7 +12,6 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectModel } from '@nestjs/mongoose';
import { Document, Model, Query, Types } from 'mongoose';
import { config } from '@/config';
import { I18nService } from '@/i18n/services/i18n.service';
import { BaseRepository } from '@/utils/generics/base-repository';
@@ -65,8 +64,7 @@ export class SettingRepository extends BaseRepository<Setting> {
* Emits an event after a `Setting` has been updated.
*
* This method is used to synchronize global settings by emitting an event
* based on the `group` and `label` of the `Setting`. It also updates the i18n
* default language setting when the `default_lang` label is updated.
* based on the `group` and `label` of the `Setting`.
*
* @param _query The Mongoose query object used to find and update the document.
* @param setting The updated `Setting` object.
@@ -86,33 +84,5 @@ export class SettingRepository extends BaseRepository<Setting> {
'hook:settings:' + setting.group + ':' + setting.label,
setting,
);
if (setting.label === 'default_lang') {
// @todo : check if this actually updates the default lang
this.i18n.resolveLanguage(setting.value as string);
}
}
/**
* Sets default values before creating a `Setting` document.
*
* If the setting is part of the `nlp_settings` group, it sets specific values
* for `languages` and `default_lang` labels, using configuration values from the
* chatbot settings.
*
* @param setting The `Setting` document to be created.
*/
async preCreate(
setting: Document<unknown, unknown, Setting> &
Setting & { _id: Types.ObjectId },
) {
if (setting.group === 'nlp_settings') {
if (setting.label === 'languages') {
setting.value = config.chatbot.lang.available;
} else if (setting.label === 'default_lang') {
setting.value = config.chatbot.lang.default;
setting.options = config.chatbot.lang.available;
}
}
}
}

View File

@@ -98,8 +98,6 @@ export type SettingDict = { [group: string]: Setting[] };
export type Settings = {
nlp_settings: {
default_lang: string;
languages: string[];
threshold: string;
provider: string;
endpoint: string;

View File

@@ -7,8 +7,6 @@
* 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 { config } from '@/config';
import { SettingCreateDto } from '../dto/setting.dto';
import { SettingType } from '../schemas/types';
@@ -67,26 +65,6 @@ export const settingModels: SettingCreateDto[] = [
type: SettingType.text,
weight: 3,
},
{
group: 'nlp_settings',
label: 'languages',
value: [],
options: [],
type: SettingType.select,
config: {
multiple: true,
allowCreate: true,
},
weight: 4,
},
{
group: 'nlp_settings',
label: 'default_lang',
value: config.chatbot.lang.default,
options: [], // NOTE : will be set onBeforeCreate from config
type: SettingType.select,
weight: 5,
},
{
group: 'nlp_settings',
label: 'threshold',
@@ -97,7 +75,7 @@ export const settingModels: SettingCreateDto[] = [
max: 1,
step: 0.01,
},
weight: 6,
weight: 4,
},
{
group: 'contact',

View File

@@ -23,7 +23,10 @@ import { SentMessageInfo } from 'nodemailer';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { installUserFixtures } from '@/utils/test/fixtures/user';
import {
@@ -69,6 +72,7 @@ describe('AuthController', () => {
PermissionModel,
InvitationModel,
AttachmentModel,
LanguageModel,
]),
],
providers: [
@@ -86,6 +90,8 @@ describe('AuthController', () => {
PermissionRepository,
InvitationRepository,
InvitationService,
LanguageRepository,
LanguageService,
JwtService,
{
provide: MailerService,

View File

@@ -20,7 +20,10 @@ import { SentMessageInfo } from 'nodemailer';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
import { installPermissionFixtures } from '@/utils/test/fixtures/permission';
@@ -75,6 +78,7 @@ describe('UserController', () => {
PermissionModel,
InvitationModel,
AttachmentModel,
LanguageModel,
]),
JwtModule,
],
@@ -108,6 +112,8 @@ describe('UserController', () => {
},
AttachmentService,
AttachmentRepository,
LanguageService,
LanguageRepository,
ValidateAccountService,
{
provide: I18nService,

View File

@@ -16,7 +16,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { ISendMailOptions, MailerService } from '@nestjs-modules/mailer';
import { SentMessageInfo } from 'nodemailer';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { IGNORED_TEST_FIELDS } from '@/utils/test/constants';
import {
@@ -55,6 +58,7 @@ describe('InvitationService', () => {
RoleModel,
PermissionModel,
InvitationModel,
LanguageModel,
]),
JwtModule,
],
@@ -66,6 +70,8 @@ describe('InvitationService', () => {
PermissionRepository,
InvitationRepository,
InvitationService,
LanguageRepository,
LanguageService,
JwtService,
Logger,
{

View File

@@ -18,6 +18,7 @@ import { MailerService } from '@nestjs-modules/mailer';
import { config } from '@/config';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { BaseService } from '@/utils/generics/base-service';
@@ -42,6 +43,7 @@ export class InvitationService extends BaseService<
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService,
protected readonly i18n: I18nService,
public readonly languageService: LanguageService,
) {
super(repository);
}
@@ -63,6 +65,7 @@ export class InvitationService extends BaseService<
const jwt = await this.sign(dto);
if (this.mailerService) {
try {
const defaultLanguage = await this.languageService.getDefaultLanguage();
await this.mailerService.sendMail({
to: dto.email,
template: 'invitation.mjml',
@@ -70,7 +73,7 @@ export class InvitationService extends BaseService<
token: jwt,
// TODO: Which language should we use?
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
this.i18n.t(key, { lang: defaultLanguage.code }),
},
subject: this.i18n.t('invitation_subject'),
});

View File

@@ -21,7 +21,10 @@ import { SentMessageInfo } from 'nodemailer';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
import {
@@ -52,6 +55,7 @@ describe('PasswordResetService', () => {
RoleModel,
PermissionModel,
AttachmentModel,
LanguageModel,
]),
JwtModule,
],
@@ -62,6 +66,8 @@ describe('PasswordResetService', () => {
AttachmentService,
AttachmentRepository,
RoleRepository,
LanguageService,
LanguageRepository,
LoggerService,
PasswordResetService,
JwtService,

View File

@@ -22,6 +22,7 @@ import { compareSync } from 'bcryptjs';
import { config } from '@/config';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { UserService } from './user.service';
@@ -35,6 +36,7 @@ export class PasswordResetService {
private logger: LoggerService,
private readonly userService: UserService,
public readonly i18n: I18nService,
public readonly languageService: LanguageService,
) {}
public readonly jwtSignOptions: JwtSignOptions = {
@@ -59,6 +61,7 @@ export class PasswordResetService {
if (this.mailerService) {
try {
const defaultLanguage = await this.languageService.getDefaultLanguage();
await this.mailerService.sendMail({
to: dto.email,
template: 'password_reset.mjml',
@@ -66,7 +69,7 @@ export class PasswordResetService {
token: jwt,
first_name: user.first_name,
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
this.i18n.t(key, { lang: defaultLanguage.code }),
},
subject: this.i18n.t('password_reset_subject'),
});

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.
*/
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { JwtModule } from '@nestjs/jwt';
import { MongooseModule } from '@nestjs/mongoose';
@@ -17,7 +18,10 @@ import { SentMessageInfo } from 'nodemailer';
import { AttachmentRepository } from '@/attachment/repositories/attachment.repository';
import { AttachmentModel } from '@/attachment/schemas/attachment.schema';
import { AttachmentService } from '@/attachment/services/attachment.service';
import { LanguageRepository } from '@/i18n/repositories/language.repository';
import { LanguageModel } from '@/i18n/schemas/language.schema';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { installUserFixtures, users } from '@/utils/test/fixtures/user';
import {
@@ -46,6 +50,7 @@ describe('ValidateAccountService', () => {
RoleModel,
PermissionModel,
AttachmentModel,
LanguageModel,
]),
JwtModule,
],
@@ -56,6 +61,8 @@ describe('ValidateAccountService', () => {
UserRepository,
RoleService,
RoleRepository,
LanguageService,
LanguageRepository,
LoggerService,
{
provide: MailerService,
@@ -74,6 +81,14 @@ describe('ValidateAccountService', () => {
t: jest.fn().mockImplementation((t) => t),
},
},
{
provide: CACHE_MANAGER,
useValue: {
del: jest.fn(),
get: jest.fn(),
set: jest.fn(),
},
},
],
}).compile();
validateAccountService = module.get<ValidateAccountService>(

View File

@@ -19,6 +19,8 @@ import { MailerService } from '@nestjs-modules/mailer';
import { config } from '@/config';
import { I18nService } from '@/i18n/services/i18n.service';
import { LanguageService } from '@/i18n/services/language.service';
import { LoggerService } from '@/logger/logger.service';
import { UserService } from './user.service';
import { UserCreateDto } from '../dto/user.dto';
@@ -35,7 +37,9 @@ export class ValidateAccountService {
@Inject(JwtService) private readonly jwtService: JwtService,
private readonly userService: UserService,
@Optional() private readonly mailerService: MailerService | undefined,
private logger: LoggerService,
private readonly i18n: I18nService,
private readonly languageService: LanguageService,
) {}
/**
@@ -73,17 +77,28 @@ export class ValidateAccountService {
const confirmationToken = await this.sign({ email: dto.email });
if (this.mailerService) {
await this.mailerService.sendMail({
to: dto.email,
template: 'account_confirmation.mjml',
context: {
token: confirmationToken,
first_name: dto.first_name,
t: (key: string) =>
this.i18n.t(key, { lang: config.chatbot.lang.default }),
},
subject: this.i18n.t('account_confirmation_subject'),
});
try {
const defaultLanguage = await this.languageService.getDefaultLanguage();
await this.mailerService.sendMail({
to: dto.email,
template: 'account_confirmation.mjml',
context: {
token: confirmationToken,
first_name: dto.first_name,
t: (key: string) =>
this.i18n.t(key, { lang: defaultLanguage.code }),
},
subject: this.i18n.t('account_confirmation_subject'),
});
} catch (e) {
this.logger.error(
'Could not send email',
e.message,
e.stack,
'ValidateAccount',
);
throw new InternalServerErrorException('Could not send email');
}
}
}