remove singleton usage of i18n (#14422)
closes https://github.com/twentyhq/core-team-issues/issues/1480 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
+1
@@ -249,6 +249,7 @@ describe('ApprovedAccessDomainService', () => {
|
||||
const sender = {
|
||||
userEmail: 'sender@example.com',
|
||||
name: { firstName: 'John', lastName: 'Doe' },
|
||||
locale: 'en',
|
||||
} as WorkspaceMemberWorkspaceEntity;
|
||||
const workspace = {
|
||||
displayName: 'Test Workspace',
|
||||
|
||||
@@ -26,6 +26,7 @@ import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/ser
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@@ -134,6 +135,14 @@ describe('AuthService', () => {
|
||||
findWorkspaceFromWorkspaceIdOrAuthProvider: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
getI18nInstance: jest.fn().mockReturnValue({
|
||||
_: jest.fn().mockReturnValue('mocked-translation'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { t, msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@@ -54,6 +53,7 @@ import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
@@ -87,6 +87,7 @@ export class AuthService {
|
||||
private readonly emailService: EmailService,
|
||||
@InjectRepository(AppToken)
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
private async checkAccessAndUseInvitationOrThrow(
|
||||
@@ -485,14 +486,16 @@ export class AuthService {
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
|
||||
i18n.activate(firstUserWorkspace.locale);
|
||||
const passwordChangedMsg = msg`Your Password Has Been Successfully Changed`;
|
||||
const i18n = this.i18nService.getI18nInstance(firstUserWorkspace.locale);
|
||||
const subject = i18n._(passwordChangedMsg);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${this.twentyConfigService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: user.email,
|
||||
subject: t`Your Password Has Been Successfully Changed`,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
+9
@@ -14,6 +14,7 @@ import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
import { ResetPasswordService } from './reset-password.service';
|
||||
|
||||
@@ -67,6 +68,14 @@ describe('ResetPasswordService', () => {
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
getI18nInstance: jest.fn().mockReturnValue({
|
||||
_: jest.fn().mockReturnValue('mocked-translation'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
+7
-4
@@ -3,8 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@@ -28,6 +27,7 @@ import { type PasswordResetToken } from 'src/engine/core-modules/auth/dto/token.
|
||||
import { type ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -45,6 +45,7 @@ export class ResetPasswordService {
|
||||
@InjectRepository(AppToken)
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
private readonly emailService: EmailService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async generatePasswordResetToken(
|
||||
@@ -165,14 +166,16 @@ export class ResetPasswordService {
|
||||
const html = render(emailTemplate, { pretty: true });
|
||||
const text = render(emailTemplate, { plainText: true });
|
||||
|
||||
i18n.activate(locale);
|
||||
const resetPasswordMsg = msg`Action Needed to Reset Password`;
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
const subject = i18n._(resetPasswordMsg);
|
||||
|
||||
this.emailService.send({
|
||||
from: `${this.twentyConfigService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: email,
|
||||
subject: t`Action Needed to Reset Password`,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
+8
-4
@@ -1,8 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds, differenceInMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
EmailVerificationExceptionCode,
|
||||
} from 'src/engine/core-modules/email-verification/email-verification.exception';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
|
||||
@@ -37,6 +37,7 @@ export class EmailVerificationService {
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly userService: UserService,
|
||||
private readonly emailVerificationTokenService: EmailVerificationTokenService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async sendVerificationEmail(
|
||||
@@ -84,13 +85,16 @@ export class EmailVerificationService {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
i18n.activate(locale);
|
||||
const emailVerificationMsg = msg`Welcome to Twenty: Please Confirm Your Email`;
|
||||
const i18n = this.i18nService.getI18nInstance(locale);
|
||||
const subject = i18n._(emailVerificationMsg);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${this.twentyConfigService.get(
|
||||
'EMAIL_FROM_NAME',
|
||||
)} <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: email,
|
||||
subject: t`Welcome to Twenty: Please Confirm Your Email`,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Injectable, type NestMiddleware } from '@nestjs/common';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { type NextFunction, type Request, type Response } from 'express';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
// TODO: this should be deprecated as singleton pattern won't work: user will keep changing locales for eachothers
|
||||
@Injectable()
|
||||
export class I18nMiddleware implements NestMiddleware {
|
||||
use(req: Request, _res: Response, next: NextFunction) {
|
||||
const locale = req.locale;
|
||||
|
||||
if (locale && Object.values(APP_LOCALES).includes(locale)) {
|
||||
i18n.activate(locale);
|
||||
} else {
|
||||
i18n.activate(SOURCE_LOCALE);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Global,
|
||||
type MiddlewareConsumer,
|
||||
Module,
|
||||
type NestModule,
|
||||
} from '@nestjs/common';
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
|
||||
import { I18nMiddleware } from 'src/engine/core-modules/i18n/i18n.middleware';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
@Global()
|
||||
@@ -13,8 +7,4 @@ import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
providers: [I18nService],
|
||||
exports: [I18nService],
|
||||
})
|
||||
export class I18nModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(I18nMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
export class I18nModule {}
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
type I18n,
|
||||
type MessageOptions,
|
||||
type Messages,
|
||||
i18n,
|
||||
setupI18n,
|
||||
} from '@lingui/core';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
@@ -90,14 +89,7 @@ export class I18nService implements OnModuleInit {
|
||||
localeI18n.activate(locale);
|
||||
|
||||
this.i18nInstancesMap[locale] = localeI18n;
|
||||
|
||||
// TODO: deprecate this line which is legacy as soon as we only use the i18nInstancesMap
|
||||
// Also deprecate i18n.middleware.ts
|
||||
i18n.load(locale, messages);
|
||||
});
|
||||
|
||||
// TODO: deprecate this line which is legacy as soon as we only use the i18nInstancesMap
|
||||
i18n.activate(SOURCE_LOCALE);
|
||||
}
|
||||
|
||||
getI18nInstance(locale: keyof typeof APP_LOCALES) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
|
||||
import { RequestLocale } from 'src/engine/decorators/locale/request-locale.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
|
||||
@Controller('rest/metadata/views')
|
||||
@@ -40,6 +41,7 @@ export class ViewController {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@@ -163,6 +165,7 @@ export class ViewController {
|
||||
const objectMetadata = objectMetadataMaps.byId[view.objectMetadataId];
|
||||
|
||||
if (objectMetadata) {
|
||||
const i18n = this.i18nService.getI18nInstance(locale ?? 'en');
|
||||
const translatedObjectLabel = resolveObjectMetadataStandardOverride(
|
||||
{
|
||||
labelPlural: objectMetadata.labelPlural,
|
||||
@@ -174,6 +177,7 @@ export class ViewController {
|
||||
},
|
||||
'labelPlural',
|
||||
locale,
|
||||
i18n,
|
||||
);
|
||||
|
||||
processedName = this.viewService.processViewNameWithTemplate(
|
||||
|
||||
@@ -36,6 +36,7 @@ import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
@Resolver(() => ViewDTO)
|
||||
@UseFilters(ViewGraphqlApiExceptionFilter)
|
||||
@@ -48,6 +49,7 @@ export class ViewResolver {
|
||||
private readonly viewFilterGroupService: ViewFilterGroupService,
|
||||
private readonly viewSortService: ViewSortService,
|
||||
private readonly viewGroupService: ViewGroupService,
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly viewV2Service: ViewV2Service,
|
||||
) {}
|
||||
@@ -65,6 +67,7 @@ export class ViewResolver {
|
||||
});
|
||||
|
||||
if (objectMetadata) {
|
||||
const i18n = this.i18nService.getI18nInstance(context.req.locale);
|
||||
const translatedObjectLabel = resolveObjectMetadataStandardOverride(
|
||||
{
|
||||
labelPlural: objectMetadata.labelPlural,
|
||||
@@ -76,6 +79,7 @@ export class ViewResolver {
|
||||
},
|
||||
'labelPlural',
|
||||
context.req.locale,
|
||||
i18n,
|
||||
);
|
||||
|
||||
return this.viewService.processViewNameWithTemplate(
|
||||
|
||||
+10
@@ -16,6 +16,7 @@ import { WorkspaceInvitationException } from 'src/engine/core-modules/workspace-
|
||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
|
||||
import { WorkspaceInvitationService } from './workspace-invitation.service';
|
||||
|
||||
@@ -83,6 +84,14 @@ describe('WorkspaceInvitationService', () => {
|
||||
// Add other methods as needed
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: {
|
||||
getI18nInstance: jest.fn().mockReturnValue({
|
||||
_: jest.fn().mockReturnValue('mocked-translation'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -152,6 +161,7 @@ describe('WorkspaceInvitationService', () => {
|
||||
const sender = {
|
||||
userEmail: 'sender@example.com',
|
||||
name: { firstName: 'Sender' },
|
||||
locale: 'en',
|
||||
};
|
||||
|
||||
jest.spyOn(service, 'createWorkspaceInvitation').mockResolvedValue({
|
||||
|
||||
+7
-4
@@ -3,8 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { i18n } from '@lingui/core';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { render } from '@react-email/render';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
@@ -23,6 +22,7 @@ import {
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -46,6 +46,7 @@ export class WorkspaceInvitationService {
|
||||
private readonly emailService: EmailService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async validatePersonalInvitation({
|
||||
@@ -309,12 +310,14 @@ export class WorkspaceInvitationService {
|
||||
plainText: true,
|
||||
});
|
||||
|
||||
i18n.activate(sender.locale);
|
||||
const joinTeamMsg = msg`Join your team on Twenty`;
|
||||
const i18n = this.i18nService.getI18nInstance(sender.locale);
|
||||
const subject = i18n._(joinTeamMsg);
|
||||
|
||||
await this.emailService.send({
|
||||
from: `${sender.name.firstName} ${sender.name.lastName} (via Twenty) <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`,
|
||||
to: invitation.value.email,
|
||||
subject: t`Join your team on Twenty`,
|
||||
subject,
|
||||
text,
|
||||
html,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user