Fix i18n issues on datamodel translation (#13710)

As per title, I believe calling i18n is bad because it's a singleton.

This singleton is being set back and force by users in
i18n.middleware.ts

Strategy:
we need to load the translations dynamically! Should be straight
forward:
- singleton (i18nService) containing all translationMessages
- singleton also containing a map of i18n instances loaded for each
locale => I've checked it's not heavy on RAM at all
- some methods to get the translation for a messageId x locale or the
whole i18n instance

==> use it everywhere, this PR only takes care of data model translation
This commit is contained in:
Charles Bochet
2025-08-07 01:28:40 +02:00
committed by GitHub
parent 58d25ebd83
commit fecf8ca913
5 changed files with 79 additions and 14 deletions
@@ -4,6 +4,7 @@ import { i18n } from '@lingui/core';
import { NextFunction, Request, 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) {
@@ -1,6 +1,6 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { i18n } from '@lingui/core';
import { I18n, MessageOptions, Messages, i18n, setupI18n } from '@lingui/core';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { messages as afMessages } from 'src/engine/core-modules/i18n/locales/generated/af-ZA';
@@ -37,9 +37,11 @@ import { messages as zhHantMessages } from 'src/engine/core-modules/i18n/locales
@Injectable()
export class I18nService implements OnModuleInit {
private i18nInstancesMap: Record<keyof typeof APP_LOCALES, I18n> =
{} as Record<keyof typeof APP_LOCALES, I18n>;
async loadTranslations() {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messages: Record<keyof typeof APP_LOCALES, any> = {
const messagesByLocale: Record<keyof typeof APP_LOCALES, Messages> = {
en: enMessages,
'pseudo-en': pseudoEnMessages,
'af-ZA': afMessages,
@@ -73,16 +75,45 @@ export class I18nService implements OnModuleInit {
'zh-TW': zhHantMessages,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(Object.entries(messages) as [keyof typeof APP_LOCALES, any][]).forEach(
([locale, message]) => {
i18n.load(locale, message);
},
);
(
Object.entries(messagesByLocale) as [keyof typeof APP_LOCALES, Messages][]
).forEach(([locale, messages]) => {
const localeI18n = setupI18n();
localeI18n.load(locale, messages);
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) {
return this.i18nInstancesMap[locale];
}
translateMessage({
messageId,
values,
locale = SOURCE_LOCALE,
options,
}: {
messageId: string;
values?: Record<string, string>;
locale?: keyof typeof APP_LOCALES;
options?: MessageOptions;
}) {
const i18n = this.getI18nInstance(locale);
return i18n._(messageId, values, options);
}
async onModuleInit() {
this.loadTranslations();
}