From 4b93c277bdaefdfce65b7c2e6034455945450577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 8 Apr 2026 14:13:45 +0200 Subject: [PATCH] fix: stop "Uncompiled message detected" log flood from standard label translation Resolvers re-hashed English labels via generateMessageId() and called i18n._() against catalogs missing those ids, triggering >500 warnings/sec on self-hosted instances (#19423). Stores real MessageDescriptors (with extractor-computed ids) in a message-keyed registry populated as a side effect of i18nLabel(msg`...`). Resolvers look up the descriptor and only call i18n._() when the catalog actually has the id, otherwise fall back to descriptor.message. A module-load pass with placeholder args runs the standard object/field builders once at boot so the registry is populated even on server restart (workspace sync only runs builders for new workspaces). Co-Authored-By: Claude Opus 4.6 --- .../engine/core-modules/i18n/i18n.service.ts | 1 + ...ember-standard-metadata-descriptor.util.ts | 15 + ...rd-metadata-descriptor-by-message.state.ts | 6 + .../translate-standard-metadata-label.util.ts | 28 ++ .../i18n/utils/generateMessageId.ts | 11 - ...ld-metadata-standard-override.util.spec.ts | 258 ++++--------- ...e-field-metadata-standard-override.util.ts | 35 +- ...ct-metadata-standard-override.util.spec.ts | 350 +++++------------- ...-object-metadata-standard-override.util.ts | 35 +- .../standard-page-layout-tab-titles.ts | 33 +- ...resolve-page-layout-tab-title.util.spec.ts | 72 ++-- .../resolve-page-layout-tab-title.util.ts | 11 +- .../view/services/view.service.ts | 34 +- .../utils/i18n-label.util.ts | 9 +- ...e-standard-metadata-descriptor-registry.ts | 31 ++ 15 files changed, 329 insertions(+), 600 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state.ts create mode 100644 packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/i18n/utils/generateMessageId.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/populate-standard-metadata-descriptor-registry.ts diff --git a/packages/twenty-server/src/engine/core-modules/i18n/i18n.service.ts b/packages/twenty-server/src/engine/core-modules/i18n/i18n.service.ts index 4c8ca4e78dd..e559a70571d 100644 --- a/packages/twenty-server/src/engine/core-modules/i18n/i18n.service.ts +++ b/packages/twenty-server/src/engine/core-modules/i18n/i18n.service.ts @@ -39,6 +39,7 @@ import { messages as ukMessages } from 'src/engine/core-modules/i18n/locales/gen import { messages as viMessages } from 'src/engine/core-modules/i18n/locales/generated/vi-VN'; import { messages as zhHansMessages } from 'src/engine/core-modules/i18n/locales/generated/zh-CN'; import { messages as zhHantMessages } from 'src/engine/core-modules/i18n/locales/generated/zh-TW'; +import 'src/engine/workspace-manager/twenty-standard-application/utils/populate-standard-metadata-descriptor-registry'; @Injectable() export class I18nService implements OnModuleInit { diff --git a/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util.ts b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util.ts new file mode 100644 index 00000000000..ee8c0a85b05 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util.ts @@ -0,0 +1,15 @@ +import { type MessageDescriptor } from '@lingui/core'; + +import { standardMetadataDescriptorByMessage } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state'; + +export const rememberStandardMetadataDescriptor = ( + descriptor: MessageDescriptor, +): void => { + const message = descriptor.message; + + if (message === undefined || message === '') { + return; + } + + standardMetadataDescriptorByMessage.set(message, descriptor); +}; diff --git a/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state.ts b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state.ts new file mode 100644 index 00000000000..76ccbe6fc59 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state.ts @@ -0,0 +1,6 @@ +import { type MessageDescriptor } from '@lingui/core'; + +export const standardMetadataDescriptorByMessage = new Map< + string, + MessageDescriptor +>(); diff --git a/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util.ts b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util.ts new file mode 100644 index 00000000000..aebd118d3e1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util.ts @@ -0,0 +1,28 @@ +import { type I18n } from '@lingui/core'; + +import { standardMetadataDescriptorByMessage } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/standard-metadata-descriptor-by-message.state'; + +// Skips Lingui's `_()` when the catalog is missing the id, which would +// otherwise trigger "Uncompiled message detected" warnings. +export const translateStandardMetadataLabel = ( + i18n: I18n, + label: string | null | undefined, +): string | undefined => { + if (label == null || label === '') { + return undefined; + } + + const descriptor = standardMetadataDescriptorByMessage.get(label); + + if (descriptor === undefined) { + return undefined; + } + + const id = descriptor.id; + + if (typeof id === 'string' && i18n.messages?.[id] !== undefined) { + return i18n._(descriptor); + } + + return descriptor.message; +}; diff --git a/packages/twenty-server/src/engine/core-modules/i18n/utils/generateMessageId.ts b/packages/twenty-server/src/engine/core-modules/i18n/utils/generateMessageId.ts deleted file mode 100644 index 24bb133c3bf..00000000000 --- a/packages/twenty-server/src/engine/core-modules/i18n/utils/generateMessageId.ts +++ /dev/null @@ -1,11 +0,0 @@ -import crypto from 'crypto'; - -const UNIT_SEPARATOR = '\u001F'; - -export function generateMessageId(msg: string, context = '') { - return crypto - .createHash('sha256') - .update(msg + UNIT_SEPARATOR + (context || '')) - .digest('base64') - .slice(0, 6); -} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/resolve-field-metadata-standard-override.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/resolve-field-metadata-standard-override.util.spec.ts index 99e324ea5b8..e9d73160670 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/resolve-field-metadata-standard-override.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/__tests__/resolve-field-metadata-standard-override.util.spec.ts @@ -1,15 +1,36 @@ import { type I18n } from '@lingui/core'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; +import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util'; -jest.mock('src/engine/core-modules/i18n/utils/generateMessageId'); +jest.mock( + 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util', + () => ({ + translateStandardMetadataLabel: jest.fn(), + }), +); -const mockGenerateMessageId = generateMessageId as jest.MockedFunction< - typeof generateMessageId +const mockTranslateStandardMetadataLabel = + translateStandardMetadataLabel as jest.MockedFunction< + typeof translateStandardMetadataLabel + >; + +type FieldFixture = Pick< + FieldMetadataDTO, + 'label' | 'description' | 'icon' | 'isCustom' | 'standardOverrides' >; +const baseFixture = (overrides: Partial = {}): FieldFixture => ({ + label: 'Standard Label', + description: 'Standard Description', + icon: 'default-icon', + isCustom: false, + standardOverrides: undefined, + ...overrides, +}); + describe('resolveFieldMetadataStandardOverride', () => { let mockI18n: jest.Mocked; @@ -18,17 +39,15 @@ describe('resolveFieldMetadataStandardOverride', () => { mockI18n = { _: jest.fn(), } as unknown as jest.Mocked; + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); }); describe('Custom fields', () => { it('should return the field value for custom label field', () => { - const fieldMetadata = { + const fieldMetadata = baseFixture({ label: 'Custom Label', - description: 'Custom Description', - icon: 'custom-icon', isCustom: true, - standardOverrides: undefined, - }; + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -41,13 +60,10 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should return the field value for custom description field', () => { - const fieldMetadata = { - label: 'Custom Label', + const fieldMetadata = baseFixture({ description: 'Custom Description', - icon: 'custom-icon', isCustom: true, - standardOverrides: undefined, - }; + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -60,13 +76,10 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should return the field value for custom icon field', () => { - const fieldMetadata = { - label: 'Custom Label', - description: 'Custom Description', + const fieldMetadata = baseFixture({ icon: 'custom-icon', isCustom: true, - standardOverrides: undefined, - }; + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -81,15 +94,9 @@ describe('resolveFieldMetadataStandardOverride', () => { describe('Standard fields - Icon overrides', () => { it('should return override icon when available for standard field', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - icon: 'override-icon', - }, - }; + const fieldMetadata = baseFixture({ + standardOverrides: { icon: 'override-icon' }, + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -104,11 +111,7 @@ describe('resolveFieldMetadataStandardOverride', () => { describe('Standard fields - Translation overrides', () => { it('should return translation override when available for non-icon fields', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { translations: { 'fr-FR': { @@ -117,7 +120,7 @@ describe('resolveFieldMetadataStandardOverride', () => { }, }, }, - }; + }); expect( resolveFieldMetadataStandardOverride( @@ -138,22 +141,13 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should fallback when translation override is not available for the locale', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { translations: { - 'es-ES': { - label: 'Etiqueta en español', - }, + 'es-ES': { label: 'Etiqueta en español' }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -166,22 +160,13 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should fallback when translation override is not available for the labelKey', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { translations: { - 'fr-FR': { - label: 'Libellé traduit', - }, + 'fr-FR': { label: 'Libellé traduit' }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -194,22 +179,13 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should not use translation overrides when locale is undefined', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { translations: { - 'fr-FR': { - label: 'Libellé traduit', - }, + 'fr-FR': { label: 'Libellé traduit' }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -224,17 +200,13 @@ describe('resolveFieldMetadataStandardOverride', () => { describe('Standard fields - SOURCE_LOCALE overrides', () => { it('should return direct override for SOURCE_LOCALE when available', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { label: 'Overridden Label', description: 'Overridden Description', icon: 'overridden-icon', }, - }; + }); expect( resolveFieldMetadataStandardOverride( @@ -263,18 +235,9 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should not use direct override for non-SOURCE_LOCALE', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - label: 'Overridden Label', - }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + const fieldMetadata = baseFixture({ + standardOverrides: { label: 'Overridden Label' }, + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -287,18 +250,9 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should not use empty string override for SOURCE_LOCALE', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - label: '', - }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + const fieldMetadata = baseFixture({ + standardOverrides: { label: '' }, + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -311,18 +265,9 @@ describe('resolveFieldMetadataStandardOverride', () => { }); it('should not use undefined override for SOURCE_LOCALE', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - label: undefined, - }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + const fieldMetadata = baseFixture({ + standardOverrides: { label: undefined }, + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -335,47 +280,31 @@ describe('resolveFieldMetadataStandardOverride', () => { }); }); - describe('Standard fields - Auto translation fallback', () => { - it('should return translated message when translation is available', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: undefined, - }; - - mockGenerateMessageId.mockReturnValue('standard.label.message.id'); - mockI18n._.mockReturnValue('Libellé traduit automatiquement'); + describe('Standard fields - Registry-based translation', () => { + it('should return translated label when the registry has the descriptor', () => { + mockTranslateStandardMetadataLabel.mockReturnValue( + 'Libellé traduit automatiquement', + ); const result = resolveFieldMetadataStandardOverride( - fieldMetadata, + baseFixture(), 'label', 'fr-FR', mockI18n, ); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label'); - expect(mockI18n._).toHaveBeenCalledWith('standard.label.message.id'); + expect(mockTranslateStandardMetadataLabel).toHaveBeenCalledWith( + mockI18n, + 'Standard Label', + ); expect(result).toBe('Libellé traduit automatiquement'); }); - it('should return original field value when no translation is found', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: undefined, - }; - - const messageId = 'standard.label.message.id'; - - mockGenerateMessageId.mockReturnValue(messageId); - mockI18n._.mockReturnValue(messageId); + it('should return original field value when registry has no descriptor', () => { + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); const result = resolveFieldMetadataStandardOverride( - fieldMetadata, + baseFixture(), 'label', 'fr-FR', mockI18n, @@ -387,20 +316,14 @@ describe('resolveFieldMetadataStandardOverride', () => { describe('Priority order - Standard fields', () => { it('should prioritize translation override over SOURCE_LOCALE override for non-SOURCE_LOCALE', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const fieldMetadata = baseFixture({ standardOverrides: { label: 'Source Override', translations: { - 'fr-FR': { - label: 'Translation Override', - }, + 'fr-FR': { label: 'Translation Override' }, }, }, - }; + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -410,20 +333,13 @@ describe('resolveFieldMetadataStandardOverride', () => { ); expect(result).toBe('Translation Override'); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); }); it('should prioritize SOURCE_LOCALE override over auto translation for SOURCE_LOCALE', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - label: 'Source Override', - }, - }; + const fieldMetadata = baseFixture({ + standardOverrides: { label: 'Source Override' }, + }); const result = resolveFieldMetadataStandardOverride( fieldMetadata, @@ -433,32 +349,20 @@ describe('resolveFieldMetadataStandardOverride', () => { ); expect(result).toBe('Source Override'); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); }); - it('should use auto translation when no overrides are available', () => { - const fieldMetadata = { - label: 'Standard Label', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: {}, - }; - - mockGenerateMessageId.mockReturnValue('auto.translation.id'); - mockI18n._.mockReturnValue('Auto Translated Label'); + it('should use registry translation when no overrides are available', () => { + mockTranslateStandardMetadataLabel.mockReturnValue('Auto Translated Label'); const result = resolveFieldMetadataStandardOverride( - fieldMetadata, + baseFixture({ standardOverrides: {} }), 'label', 'de-DE', mockI18n, ); expect(result).toBe('Auto Translated Label'); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label'); - expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id'); }); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts index 807bcc70d2f..6f32e362fa0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util.ts @@ -3,10 +3,9 @@ import { isNonEmptyString } from '@sniptt/guards'; import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; -// TODO simplify export const resolveFieldMetadataStandardOverride = ( fieldMetadata: Pick< FieldMetadataDTO, @@ -16,24 +15,22 @@ export const resolveFieldMetadataStandardOverride = ( locale: keyof typeof APP_LOCALES | undefined, i18nInstance: I18n, ): string => { + const baseValue = fieldMetadata[labelKey] ?? ''; + if (fieldMetadata.isCustom) { - return fieldMetadata[labelKey] ?? ''; + return baseValue; } - if (labelKey === 'icon' && isDefined(fieldMetadata.standardOverrides?.icon)) { - return fieldMetadata.standardOverrides.icon; + if (labelKey === 'icon') { + return fieldMetadata.standardOverrides?.icon ?? baseValue; } - if ( - isDefined(fieldMetadata.standardOverrides?.translations) && - isDefined(locale) && - labelKey !== 'icon' - ) { - const translationValue = - fieldMetadata.standardOverrides.translations[locale]?.[labelKey]; + if (isDefined(locale)) { + const translationOverride = + fieldMetadata.standardOverrides?.translations?.[locale]?.[labelKey]; - if (isDefined(translationValue)) { - return translationValue; + if (isDefined(translationOverride)) { + return translationOverride; } } @@ -44,13 +41,5 @@ export const resolveFieldMetadataStandardOverride = ( return fieldMetadata.standardOverrides[labelKey] ?? ''; } - const messageId = generateMessageId(fieldMetadata[labelKey] ?? ''); - - const translatedMessage = i18nInstance._(messageId); - - if (translatedMessage === messageId) { - return fieldMetadata[labelKey] ?? ''; - } - - return translatedMessage; + return translateStandardMetadataLabel(i18nInstance, baseValue) ?? baseValue; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/resolve-object-metadata-standard-override.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/resolve-object-metadata-standard-override.util.spec.ts index 745d13a2622..68a8f7b86a6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/resolve-object-metadata-standard-override.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/__tests__/resolve-object-metadata-standard-override.util.spec.ts @@ -1,16 +1,46 @@ import { type I18n } from '@lingui/core'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; -jest.mock('src/engine/core-modules/i18n/utils/generateMessageId'); +jest.mock( + 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util', + () => ({ + translateStandardMetadataLabel: jest.fn(), + }), +); -const mockGenerateMessageId = generateMessageId as jest.MockedFunction< - typeof generateMessageId +const mockTranslateStandardMetadataLabel = + translateStandardMetadataLabel as jest.MockedFunction< + typeof translateStandardMetadataLabel + >; + +type ObjectFixture = Pick< + ObjectMetadataDTO, + | 'color' + | 'labelPlural' + | 'labelSingular' + | 'description' + | 'icon' + | 'isCustom' + | 'standardOverrides' >; +const baseFixture = ( + overrides: Partial = {}, +): ObjectFixture => ({ + labelSingular: 'Standard Label', + labelPlural: 'Standard Labels', + description: 'Standard Description', + icon: 'default-icon', + color: 'blue', + isCustom: false, + standardOverrides: undefined, + ...overrides, +}); + describe('resolveObjectMetadataStandardOverride', () => { let mockI18n: jest.Mocked; @@ -19,28 +49,15 @@ describe('resolveObjectMetadataStandardOverride', () => { mockI18n = { _: jest.fn(), } as unknown as jest.Mocked; + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); }); describe('Custom objects', () => { it('should return the object value for custom labelSingular object', () => { - const objectMetadata = { + const objectMetadata = baseFixture({ labelSingular: 'My Custom', - labelPlural: 'My Customs', - description: 'Custom Description', - icon: 'custom-icon', - color: 'blue', isCustom: true, - standardOverrides: undefined, - } satisfies Pick< - ObjectMetadataDTO, - | 'color' - | 'labelPlural' - | 'labelSingular' - | 'description' - | 'icon' - | 'isCustom' - | 'standardOverrides' - >; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -53,24 +70,10 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should return the object value for custom description object', () => { - const objectMetadata = { - labelSingular: 'My Custom', - labelPlural: 'My Customs', + const objectMetadata = baseFixture({ description: 'Custom Description', - icon: 'custom-icon', - color: 'blue', isCustom: true, - standardOverrides: undefined, - } satisfies Pick< - ObjectMetadataDTO, - | 'color' - | 'labelPlural' - | 'labelSingular' - | 'description' - | 'icon' - | 'isCustom' - | 'standardOverrides' - >; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -83,24 +86,10 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should return the object value for custom icon object', () => { - const objectMetadata = { - labelSingular: 'My Custom', - labelPlural: 'My Customs', - description: 'Custom Description', + const objectMetadata = baseFixture({ icon: 'custom-icon', - color: 'blue', isCustom: true, - standardOverrides: undefined, - } satisfies Pick< - ObjectMetadataDTO, - | 'color' - | 'labelPlural' - | 'labelSingular' - | 'description' - | 'icon' - | 'isCustom' - | 'standardOverrides' - >; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -113,24 +102,10 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should return the object value for custom color object', () => { - const objectMetadata = { - labelSingular: 'My Custom', - labelPlural: 'My Customs', - description: 'Custom Description', - icon: 'custom-icon', + const objectMetadata = baseFixture({ color: 'green', isCustom: true, - standardOverrides: undefined, - } satisfies Pick< - ObjectMetadataDTO, - | 'color' - | 'labelPlural' - | 'labelSingular' - | 'description' - | 'icon' - | 'isCustom' - | 'standardOverrides' - >; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -145,16 +120,9 @@ describe('resolveObjectMetadataStandardOverride', () => { describe('Standard objects - Icon overrides', () => { it('should return override icon when available for standard object', () => { - const objectMetadata = { - labelSingular: 'My Custom', - labelPlural: 'My Customs', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - icon: 'override-icon', - }, - }; + const objectMetadata = baseFixture({ + standardOverrides: { icon: 'override-icon' }, + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -169,17 +137,9 @@ describe('resolveObjectMetadataStandardOverride', () => { describe('Standard objects - Color overrides', () => { it('should return override color when available for standard object', () => { - const objectMetadata = { - labelSingular: 'Company', - labelPlural: 'Companies', - description: 'Standard Description', - icon: 'default-icon', - color: 'blue', - isCustom: false, - standardOverrides: { - color: 'red', - }, - }; + const objectMetadata = baseFixture({ + standardOverrides: { color: 'red' }, + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -192,18 +152,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should return base color when no override exists for standard object', () => { - const objectMetadata = { - labelSingular: 'Company', - labelPlural: 'Companies', - description: 'Standard Description', - icon: 'default-icon', - color: 'blue', - isCustom: false, - standardOverrides: undefined, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + const objectMetadata = baseFixture(); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -218,12 +167,7 @@ describe('resolveObjectMetadataStandardOverride', () => { describe('Standard objects - Translation overrides', () => { it('should return translation override when available for non-icon objects', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { translations: { 'fr-FR': { @@ -233,7 +177,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }, }, }, - }; + }); expect( resolveObjectMetadataStandardOverride( @@ -262,12 +206,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should fallback when translation override is not available for the locale', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { translations: { 'es-ES': { @@ -277,10 +216,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -293,12 +229,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should fallback when translation override is not available for the labelKey', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { translations: { 'fr-FR': { @@ -307,10 +238,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -323,12 +251,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should not use translation overrides when locale is undefined', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { translations: { 'fr-FR': { @@ -338,10 +261,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }, }, }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -356,19 +276,14 @@ describe('resolveObjectMetadataStandardOverride', () => { describe('Standard objects - SOURCE_LOCALE overrides', () => { it('should return direct override for SOURCE_LOCALE when available', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { labelSingular: 'Overridden Label', labelPlural: 'Overridden Labels', description: 'Overridden Description', icon: 'overridden-icon', }, - }; + }); expect( resolveObjectMetadataStandardOverride( @@ -405,17 +320,12 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should use direct override for non-SOURCE_LOCALE', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { labelSingular: 'Overridden Label', labelPlural: 'Overridden Labels', }, - }; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -428,19 +338,9 @@ describe('resolveObjectMetadataStandardOverride', () => { }); it('should not use undefined override for SOURCE_LOCALE', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - labelSingular: undefined, - }, - }; - - mockGenerateMessageId.mockReturnValue('generated-message-id'); - mockI18n._.mockReturnValue('generated-message-id'); + const objectMetadata = baseFixture({ + standardOverrides: { labelSingular: undefined }, + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -453,49 +353,31 @@ describe('resolveObjectMetadataStandardOverride', () => { }); }); - describe('Standard objects - Auto translation fallback', () => { - it('should return translated message when translation is available', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: undefined, - }; - - mockGenerateMessageId.mockReturnValue('standard.label.message.id'); - mockI18n._.mockReturnValue('Libellé traduit automatiquement'); + describe('Standard objects - Registry-based translation', () => { + it('should return translated label when the registry has the descriptor', () => { + mockTranslateStandardMetadataLabel.mockReturnValue( + 'Libellé traduit automatiquement', + ); const result = resolveObjectMetadataStandardOverride( - objectMetadata, + baseFixture(), 'labelSingular', 'fr-FR', mockI18n, ); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label'); - expect(mockI18n._).toHaveBeenCalledWith('standard.label.message.id'); + expect(mockTranslateStandardMetadataLabel).toHaveBeenCalledWith( + mockI18n, + 'Standard Label', + ); expect(result).toBe('Libellé traduit automatiquement'); }); - it('should return original object value when no translation is found', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: undefined, - }; - - const messageId = 'standard.label.message.id'; - - mockGenerateMessageId.mockReturnValue(messageId); - mockI18n._.mockReturnValue(messageId); + it('should fall back to original label when registry has no descriptor', () => { + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); const result = resolveObjectMetadataStandardOverride( - objectMetadata, + baseFixture(), 'labelSingular', 'fr-FR', mockI18n, @@ -507,12 +389,7 @@ describe('resolveObjectMetadataStandardOverride', () => { describe('Priority order - Standard objects', () => { it('should prioritize translation override over SOURCE_LOCALE override for non-SOURCE_LOCALE', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { labelSingular: 'Source Override', labelPlural: 'Source Overrides', @@ -523,7 +400,7 @@ describe('resolveObjectMetadataStandardOverride', () => { }, }, }, - }; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -533,22 +410,16 @@ describe('resolveObjectMetadataStandardOverride', () => { ); expect(result).toBe('Translation Override'); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); }); it('should prioritize SOURCE_LOCALE override over auto translation for SOURCE_LOCALE', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, + const objectMetadata = baseFixture({ standardOverrides: { labelSingular: 'Source Override', labelPlural: 'Source Overrides', }, - }; + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -558,48 +429,28 @@ describe('resolveObjectMetadataStandardOverride', () => { ); expect(result).toBe('Source Override'); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); }); - it('should use auto translation when no overrides are available', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: {}, - }; - - mockGenerateMessageId.mockReturnValue('auto.translation.id'); - mockI18n._.mockReturnValue('Auto Translated Label'); + it('should use registry translation when no overrides are available', () => { + mockTranslateStandardMetadataLabel.mockReturnValue('Auto Translated Label'); const result = resolveObjectMetadataStandardOverride( - objectMetadata, + baseFixture({ standardOverrides: {} }), 'labelSingular', 'de-DE', mockI18n, ); expect(result).toBe('Auto Translated Label'); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label'); - expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id'); }); }); describe('Undefined locale handling', () => { it('should use SOURCE_LOCALE fallback when locale is undefined for standard object', () => { - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: { - labelSingular: 'Source Override', - }, - }; + const objectMetadata = baseFixture({ + standardOverrides: { labelSingular: 'Source Override' }, + }); const result = resolveObjectMetadataStandardOverride( objectMetadata, @@ -609,33 +460,20 @@ describe('resolveObjectMetadataStandardOverride', () => { ); expect(result).toBe('Source Override'); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); }); - it('should fall back to auto translation when locale is undefined and no SOURCE_LOCALE override exists', () => { - mockI18n._.mockReturnValue('Auto Translated Label'); - mockGenerateMessageId.mockReturnValue('auto.translation.id'); - - const objectMetadata = { - labelSingular: 'Standard Label', - labelPlural: 'Standard Labels', - description: 'Standard Description', - icon: 'default-icon', - isCustom: false, - standardOverrides: undefined, - }; + it('should fall back to registry translation when locale is undefined and no SOURCE_LOCALE override exists', () => { + mockTranslateStandardMetadataLabel.mockReturnValue('Auto Translated Label'); const result = resolveObjectMetadataStandardOverride( - objectMetadata, + baseFixture(), 'labelSingular', undefined, mockI18n, ); expect(result).toBe('Auto Translated Label'); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label'); - expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id'); }); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util.ts index 22440b5c689..5fbe5889237 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util.ts @@ -3,7 +3,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; import { type ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto'; export const resolveObjectMetadataStandardOverride = ( @@ -22,41 +22,26 @@ export const resolveObjectMetadataStandardOverride = ( i18nInstance: I18n, ): string => { const safeLocale = locale ?? SOURCE_LOCALE; + const baseValue = objectMetadata[labelKey] ?? ''; if (objectMetadata.isCustom) { - return objectMetadata[labelKey] ?? ''; + return baseValue; } - if ( - (labelKey === 'icon' || labelKey === 'color') && - isDefined(objectMetadata.standardOverrides?.[labelKey]) - ) { - return objectMetadata.standardOverrides[labelKey]; + if (labelKey === 'icon' || labelKey === 'color') { + return objectMetadata.standardOverrides?.[labelKey] ?? baseValue; } - if ( - isDefined(objectMetadata.standardOverrides?.translations) && - labelKey !== 'icon' && - labelKey !== 'color' - ) { - const translationValue = - objectMetadata.standardOverrides.translations[safeLocale]?.[labelKey]; + const translationOverride = + objectMetadata.standardOverrides?.translations?.[safeLocale]?.[labelKey]; - if (isDefined(translationValue)) { - return translationValue; - } + if (isDefined(translationOverride)) { + return translationOverride; } if (isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])) { return objectMetadata.standardOverrides[labelKey] ?? ''; } - const messageId = generateMessageId(objectMetadata[labelKey] ?? ''); - const translatedMessage = i18nInstance._(messageId); - - if (translatedMessage === messageId) { - return objectMetadata[labelKey] ?? ''; - } - - return translatedMessage; + return translateStandardMetadataLabel(i18nInstance, baseValue) ?? baseValue; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/constants/standard-page-layout-tab-titles.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/constants/standard-page-layout-tab-titles.ts index b6943caedbe..fea212d1b08 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/constants/standard-page-layout-tab-titles.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/constants/standard-page-layout-tab-titles.ts @@ -1,17 +1,20 @@ -import { t } from '@lingui/core/macro'; +import { msg } from '@lingui/core/macro'; -// This file exists solely for Lingui string extraction. -// The strings defined here correspond to standard page layout tab titles -// so they appear in the .po catalogs and can be translated at resolve time -// via generateMessageId hash lookups. -export const getStandardPageLayoutTabTitles = () => [ - t`Home`, - t`Timeline`, - t`Tasks`, - t`Notes`, - t`Files`, - t`Emails`, - t`Calendar`, - t`Note`, - t`Flow`, +import { rememberStandardMetadataDescriptor } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util'; + +// MessageDescriptors (not plain strings) so the Lingui extractor picks them up. +const STANDARD_PAGE_LAYOUT_TAB_TITLE_DESCRIPTORS = [ + msg`Home`, + msg`Timeline`, + msg`Tasks`, + msg`Notes`, + msg`Files`, + msg`Emails`, + msg`Calendar`, + msg`Note`, + msg`Flow`, ]; + +for (const descriptor of STANDARD_PAGE_LAYOUT_TAB_TITLE_DESCRIPTORS) { + rememberStandardMetadataDescriptor(descriptor); +} diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/__tests__/resolve-page-layout-tab-title.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/__tests__/resolve-page-layout-tab-title.util.spec.ts index 7406a3f4d95..b37f476aa78 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/__tests__/resolve-page-layout-tab-title.util.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/__tests__/resolve-page-layout-tab-title.util.spec.ts @@ -1,14 +1,20 @@ import { type I18n } from '@lingui/core'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; -jest.mock('src/engine/core-modules/i18n/utils/generateMessageId'); +jest.mock( + 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util', + () => ({ + translateStandardMetadataLabel: jest.fn(), + }), +); -const mockGenerateMessageId = generateMessageId as jest.MockedFunction< - typeof generateMessageId ->; +const mockTranslateStandardMetadataLabel = + translateStandardMetadataLabel as jest.MockedFunction< + typeof translateStandardMetadataLabel + >; describe('resolvePageLayoutTabTitle', () => { let mockI18n: jest.Mocked; @@ -18,11 +24,11 @@ describe('resolvePageLayoutTabTitle', () => { mockI18n = { _: jest.fn(), } as unknown as jest.Mocked; + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); }); - it('should return translated title when catalog has a match', () => { - mockGenerateMessageId.mockReturnValue('abc123'); - mockI18n._.mockReturnValue('Accueil'); + it('should return translated title when registry has the descriptor', () => { + mockTranslateStandardMetadataLabel.mockReturnValue('Accueil'); const result = resolvePageLayoutTabTitle({ title: 'Home', @@ -30,14 +36,15 @@ describe('resolvePageLayoutTabTitle', () => { i18nInstance: mockI18n, }); - expect(mockGenerateMessageId).toHaveBeenCalledWith('Home'); - expect(mockI18n._).toHaveBeenCalledWith('abc123'); + expect(mockTranslateStandardMetadataLabel).toHaveBeenCalledWith( + mockI18n, + 'Home', + ); expect(result).toBe('Accueil'); }); - it('should return original title when catalog returns the hash (no translation found)', () => { - mockGenerateMessageId.mockReturnValue('xyz789'); - mockI18n._.mockReturnValue('xyz789'); + it('should return original title when no descriptor is registered for it', () => { + mockTranslateStandardMetadataLabel.mockReturnValue(undefined); const result = resolvePageLayoutTabTitle({ title: 'My Custom Tab', @@ -45,15 +52,10 @@ describe('resolvePageLayoutTabTitle', () => { i18nInstance: mockI18n, }); - expect(mockGenerateMessageId).toHaveBeenCalledWith('My Custom Tab'); - expect(mockI18n._).toHaveBeenCalledWith('xyz789'); expect(result).toBe('My Custom Tab'); }); it('should return original title for empty string', () => { - mockGenerateMessageId.mockReturnValue('empty-hash'); - mockI18n._.mockReturnValue('empty-hash'); - const result = resolvePageLayoutTabTitle({ title: '', applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier, @@ -63,38 +65,7 @@ describe('resolvePageLayoutTabTitle', () => { expect(result).toBe(''); }); - it('should translate standard tab titles', () => { - const standardTabs = [ - { source: 'Home', translated: 'Accueil' }, - { source: 'Timeline', translated: 'Chronologie' }, - { source: 'Tasks', translated: 'Tâches' }, - { source: 'Notes', translated: 'Notes' }, - { source: 'Files', translated: 'Fichiers' }, - { source: 'Emails', translated: 'E-mails' }, - { source: 'Calendar', translated: 'Calendrier' }, - { source: 'Note', translated: 'Note' }, - { source: 'Flow', translated: 'Flux' }, - ]; - - standardTabs.forEach(({ source, translated }) => { - jest.clearAllMocks(); - mockGenerateMessageId.mockReturnValue(`hash-${source}`); - mockI18n._.mockReturnValue(translated); - - const result = resolvePageLayoutTabTitle({ - title: source, - applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier, - i18nInstance: mockI18n, - }); - - expect(result).toBe(translated); - }); - }); - it('should not translate title when applicationId is not from standard app', () => { - mockGenerateMessageId.mockReturnValue('abc123'); - mockI18n._.mockReturnValue('Accueil'); - const customAppId = '11111111-1111-1111-1111-111111111111'; const result = resolvePageLayoutTabTitle({ @@ -103,8 +74,7 @@ describe('resolvePageLayoutTabTitle', () => { i18nInstance: mockI18n, }); - expect(mockGenerateMessageId).not.toHaveBeenCalled(); - expect(mockI18n._).not.toHaveBeenCalled(); + expect(mockTranslateStandardMetadataLabel).not.toHaveBeenCalled(); expect(result).toBe('Home'); }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util.ts b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util.ts index 61f2ed2d2f2..cc29fd1d8de 100644 --- a/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util.ts @@ -1,6 +1,6 @@ import { type I18n } from '@lingui/core'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; +import { translateStandardMetadataLabel } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/translate-standard-metadata-label.util'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; export const resolvePageLayoutTabTitle = ({ @@ -16,12 +16,5 @@ export const resolvePageLayoutTabTitle = ({ return title; } - const messageId = generateMessageId(title); - const translatedMessage = i18nInstance._(messageId); - - if (translatedMessage === messageId) { - return title; - } - - return translatedMessage; + return translateStandardMetadataLabel(i18nInstance, title) ?? title; }; diff --git a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts index b3f530990a9..44e00e28d40 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/services/view.service.ts @@ -1,14 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; +import { type APP_LOCALES } from 'twenty-shared/translations'; import { ViewType, ViewVisibility } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; -import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @@ -46,7 +44,6 @@ export class ViewService { private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, - private readonly i18nService: I18nService, ) {} async createOne({ @@ -393,39 +390,14 @@ export class ViewService { processViewNameWithTemplate( viewName: string, - isCustom: boolean, + _isCustom: boolean, objectLabelPlural?: string, - locale?: keyof typeof APP_LOCALES, + _locale?: keyof typeof APP_LOCALES, ): string { if (viewName.includes('{objectLabelPlural}') && objectLabelPlural) { - const messageId = generateMessageId(viewName); - const translatedTemplate = this.i18nService.translateMessage({ - messageId, - values: { - objectLabelPlural, - }, - locale: locale ?? SOURCE_LOCALE, - }); - - if (translatedTemplate !== messageId) { - return translatedTemplate; - } - return viewName.replace('{objectLabelPlural}', objectLabelPlural); } - if (!isCustom) { - const messageId = generateMessageId(viewName); - const translatedMessage = this.i18nService.translateMessage({ - messageId, - locale: locale ?? SOURCE_LOCALE, - }); - - if (translatedMessage !== messageId) { - return translatedMessage; - } - } - return viewName; } diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util.ts index 06549112dc3..5eb3ed8a27a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/i18n-label.util.ts @@ -1,4 +1,9 @@ import { type MessageDescriptor } from '@lingui/core'; -export const i18nLabel = (descriptor: MessageDescriptor): string => - descriptor.message ?? ''; +import { rememberStandardMetadataDescriptor } from 'src/engine/core-modules/i18n/standard-metadata-descriptor-registry/remember-standard-metadata-descriptor.util'; + +export const i18nLabel = (descriptor: MessageDescriptor): string => { + rememberStandardMetadataDescriptor(descriptor); + + return descriptor.message ?? ''; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/populate-standard-metadata-descriptor-registry.ts b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/populate-standard-metadata-descriptor-registry.ts new file mode 100644 index 00000000000..120aa9c59e3 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/twenty-standard-application/utils/populate-standard-metadata-descriptor-registry.ts @@ -0,0 +1,31 @@ +import 'src/engine/metadata-modules/page-layout-tab/constants/standard-page-layout-tab-titles'; + +import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; +import { buildStandardFlatFieldMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/field-metadata/build-standard-flat-field-metadata-maps.util'; +import { getStandardObjectMetadataRelatedEntityIds } from 'src/engine/workspace-manager/twenty-standard-application/utils/get-standard-object-metadata-related-entity-ids.util'; +import { buildStandardFlatObjectMetadataMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/object-metadata/build-standard-flat-object-metadata-maps.util'; + +// `i18nLabel(msg`...`)` only registers descriptors when the builder lambdas +// run. Workspace sync runs them for real workspaces, but restarts don't — +// so we do one discard-only pass at module load to keep translations working. +const PLACEHOLDER_ARGS = { + now: '', + workspaceId: '', + twentyStandardApplicationId: '', + standardObjectMetadataRelatedEntityIds: + getStandardObjectMetadataRelatedEntityIds(), +}; + +const flatObjectMetadataMaps = buildStandardFlatObjectMetadataMaps({ + ...PLACEHOLDER_ARGS, + dependencyFlatEntityMaps: { + flatFieldMetadataMaps: createEmptyFlatEntityMaps(), + }, +}); + +buildStandardFlatFieldMetadataMaps({ + ...PLACEHOLDER_ARGS, + dependencyFlatEntityMaps: { + flatObjectMetadataMaps, + }, +});