diff --git a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts index 9fd65254ef4..874e3816166 100644 --- a/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts +++ b/packages/twenty-server/src/engine/dataloaders/dataloader.service.ts @@ -22,6 +22,7 @@ import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-m import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util'; import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util'; +import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util'; import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util'; import { resolveMorphRelationsFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-morph-relations-from-flat-field-metadata.util'; import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util'; @@ -336,6 +337,8 @@ export class DataloaderService { label: flatFieldMetadata.label, description: flatFieldMetadata.description ?? undefined, icon: flatFieldMetadata.icon ?? undefined, + isCustom: + !belongsToTwentyStandardApp(flatFieldMetadata), standardOverrides: flatFieldMetadata.standardOverrides ?? undefined, }, diff --git a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util.ts b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util.ts index 76da3818f98..0aa7e08ac28 100644 --- a/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/command-menu-item/utils/build-navigation-interpolation-context.util.ts @@ -9,6 +9,7 @@ export type NavigationInterpolationObjectMetadata = { labelSingular: string; description?: string | null; icon?: string | null; + isCustom: boolean; standardOverrides?: ObjectStandardOverridesDTO | null; }; @@ -26,6 +27,7 @@ export const buildNavigationInterpolationContext = ({ labelSingular: objectMetadata.labelSingular, description: objectMetadata.description ?? undefined, icon: objectMetadata.icon ?? undefined, + isCustom: objectMetadata.isCustom, standardOverrides: objectMetadata.standardOverrides ?? undefined, }; 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 3e605bdfa6b..37721b71063 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 @@ -26,6 +26,7 @@ describe('resolveFieldMetadataStandardOverride', () => { label: 'Custom Label', description: 'Custom Description', icon: 'custom-icon', + isCustom: true, standardOverrides: undefined, }; @@ -39,11 +40,36 @@ describe('resolveFieldMetadataStandardOverride', () => { expect(result).toBe('Custom Label'); }); + it('should never translate a custom label even when it matches a standard catalog entry', () => { + const fieldMetadata = { + label: 'Status', + description: 'Custom Description', + icon: 'custom-icon', + isCustom: true, + standardOverrides: undefined, + }; + + mockGenerateMessageId.mockReturnValue('status.message.id'); + mockI18n._.mockReturnValue('Statut'); + + const result = resolveFieldMetadataStandardOverride( + fieldMetadata, + 'label', + 'fr-FR', + mockI18n, + ); + + expect(result).toBe('Status'); + expect(mockGenerateMessageId).not.toHaveBeenCalled(); + expect(mockI18n._).not.toHaveBeenCalled(); + }); + it('should return the field value for custom description field', () => { const fieldMetadata = { label: 'Custom Label', description: 'Custom Description', icon: 'custom-icon', + isCustom: true, standardOverrides: undefined, }; @@ -62,6 +88,7 @@ describe('resolveFieldMetadataStandardOverride', () => { label: 'Custom Label', description: 'Custom Description', icon: 'custom-icon', + isCustom: true, standardOverrides: undefined, }; 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 f5a51fb3559..52705f6baf8 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 @@ -10,7 +10,7 @@ import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadat export const resolveFieldMetadataStandardOverride = ( fieldMetadata: Pick< FieldMetadataDTO, - 'label' | 'description' | 'icon' | 'standardOverrides' + 'label' | 'description' | 'icon' | 'isCustom' | 'standardOverrides' >, labelKey: 'label' | 'description' | 'icon', locale: keyof typeof APP_LOCALES | undefined, @@ -18,6 +18,13 @@ export const resolveFieldMetadataStandardOverride = ( ): string => { const safeLocale = locale ?? SOURCE_LOCALE; + // Custom field labels are user-authored: never overridden nor translated. + // Without this gate, a label colliding with a standard catalog string + // (e.g. "Status") would get translated against the user's intent. + if (fieldMetadata.isCustom) { + return fieldMetadata[labelKey] ?? ''; + } + if (labelKey === 'icon' && isDefined(fieldMetadata.standardOverrides?.icon)) { return fieldMetadata.standardOverrides.icon; } diff --git a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts index 4dfeb9ea9e1..f8783f9f9b0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/minimal-metadata/minimal-metadata.service.ts @@ -78,12 +78,15 @@ export class MinimalMetadataService { .filter(isDefined) .filter((flatObjectMetadata) => flatObjectMetadata.isActive === true) .map((flatObjectMetadata) => { + const isCustom = !belongsToTwentyStandardApp(flatObjectMetadata); + const objectMetadataForOverride = { labelPlural: flatObjectMetadata.labelPlural, labelSingular: flatObjectMetadata.labelSingular, description: flatObjectMetadata.description ?? undefined, icon: flatObjectMetadata.icon ?? undefined, color: flatObjectMetadata.color ?? undefined, + isCustom, standardOverrides: flatObjectMetadata.standardOverrides ?? undefined, }; @@ -104,7 +107,7 @@ export class MinimalMetadataService { i18nInstance, ), icon: flatObjectMetadata.icon ?? undefined, - isCustom: !belongsToTwentyStandardApp(flatObjectMetadata), + isCustom, isActive: flatObjectMetadata.isActive, isSystem: flatObjectMetadata.isSystem, isRemote: flatObjectMetadata.isRemote, 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 16e237fc090..400677aa409 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 @@ -29,6 +29,7 @@ describe('resolveObjectMetadataStandardOverride', () => { description: 'Custom Description', icon: 'custom-icon', color: 'blue', + isCustom: true, standardOverrides: undefined, } satisfies Pick< ObjectMetadataDTO, @@ -37,6 +38,7 @@ describe('resolveObjectMetadataStandardOverride', () => { | 'labelSingular' | 'description' | 'icon' + | 'isCustom' | 'standardOverrides' >; @@ -50,13 +52,14 @@ describe('resolveObjectMetadataStandardOverride', () => { expect(result).toBe('My Custom'); }); - it('should return the object value for custom description object', () => { + it('should never translate a custom label even when it matches a standard catalog entry', () => { const objectMetadata = { - labelSingular: 'My Custom', - labelPlural: 'My Customs', + labelSingular: 'Company', + labelPlural: 'Companies', description: 'Custom Description', icon: 'custom-icon', color: 'blue', + isCustom: true, standardOverrides: undefined, } satisfies Pick< ObjectMetadataDTO, @@ -65,6 +68,42 @@ describe('resolveObjectMetadataStandardOverride', () => { | 'labelSingular' | 'description' | 'icon' + | 'isCustom' + | 'standardOverrides' + >; + + mockGenerateMessageId.mockReturnValue('company.message.id'); + mockI18n._.mockReturnValue('Entreprise'); + + const result = resolveObjectMetadataStandardOverride( + objectMetadata, + 'labelSingular', + 'fr-FR', + mockI18n, + ); + + expect(result).toBe('Company'); + expect(mockGenerateMessageId).not.toHaveBeenCalled(); + expect(mockI18n._).not.toHaveBeenCalled(); + }); + + it('should return the object value for custom description object', () => { + const objectMetadata = { + 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' >; @@ -85,6 +124,7 @@ describe('resolveObjectMetadataStandardOverride', () => { description: 'Custom Description', icon: 'custom-icon', color: 'blue', + isCustom: true, standardOverrides: undefined, } satisfies Pick< ObjectMetadataDTO, @@ -93,6 +133,7 @@ describe('resolveObjectMetadataStandardOverride', () => { | 'labelSingular' | 'description' | 'icon' + | 'isCustom' | 'standardOverrides' >; @@ -113,6 +154,7 @@ describe('resolveObjectMetadataStandardOverride', () => { description: 'Custom Description', icon: 'custom-icon', color: 'green', + isCustom: true, standardOverrides: undefined, } satisfies Pick< ObjectMetadataDTO, @@ -121,6 +163,7 @@ describe('resolveObjectMetadataStandardOverride', () => { | 'labelSingular' | 'description' | 'icon' + | 'isCustom' | 'standardOverrides' >; @@ -142,6 +185,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'My Customs', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { icon: 'override-icon', }, @@ -166,6 +210,7 @@ describe('resolveObjectMetadataStandardOverride', () => { description: 'Standard Description', icon: 'default-icon', color: 'blue', + isCustom: false, standardOverrides: { color: 'red', }, @@ -188,6 +233,7 @@ describe('resolveObjectMetadataStandardOverride', () => { description: 'Standard Description', icon: 'default-icon', color: 'blue', + isCustom: false, standardOverrides: undefined, }; @@ -212,6 +258,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { translations: { 'fr-FR': { @@ -255,6 +302,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { translations: { 'es-ES': { @@ -285,6 +333,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { translations: { 'fr-FR': { @@ -314,6 +363,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { translations: { 'fr-FR': { @@ -346,6 +396,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: 'Overridden Label', labelPlural: 'Overridden Labels', @@ -394,6 +445,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: 'Overridden Label', labelPlural: 'Overridden Labels', @@ -416,6 +468,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: undefined, }, @@ -442,6 +495,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: undefined, }; @@ -466,6 +520,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: undefined, }; @@ -492,6 +547,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: 'Source Override', labelPlural: 'Source Overrides', @@ -522,6 +578,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: 'Source Override', labelPlural: 'Source Overrides', @@ -546,6 +603,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: {}, }; @@ -572,6 +630,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: { labelSingular: 'Source Override', }, @@ -598,6 +657,7 @@ describe('resolveObjectMetadataStandardOverride', () => { labelPlural: 'Standard Labels', description: 'Standard Description', icon: 'default-icon', + isCustom: false, standardOverrides: undefined, }; 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 4d0633acb38..81b823cd831 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 @@ -14,6 +14,7 @@ export const resolveObjectMetadataStandardOverride = ( | 'labelSingular' | 'description' | 'icon' + | 'isCustom' | 'standardOverrides' >, labelKey: 'color' | 'labelPlural' | 'labelSingular' | 'description' | 'icon', @@ -22,6 +23,13 @@ export const resolveObjectMetadataStandardOverride = ( ): string => { const safeLocale = locale ?? SOURCE_LOCALE; + // Custom object labels are user-authored: never overridden nor translated. + // Without this gate, a label colliding with a standard catalog string + // (e.g. "Company") would get translated against the user's intent. + if (objectMetadata.isCustom) { + return objectMetadata[labelKey] ?? ''; + } + if ( (labelKey === 'icon' || labelKey === 'color') && isDefined(objectMetadata.standardOverrides?.[labelKey]) diff --git a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts index 82090117d83..2be700dedd2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/controllers/view.controller.ts @@ -25,6 +25,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util'; +import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util'; import { CreateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/create-view-permission.guard'; import { DeleteViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/delete-view-permission.guard'; import { UpdateViewPermissionGuard } from 'src/engine/metadata-modules/view-permissions/guards/update-view-permission.guard'; @@ -216,6 +217,7 @@ export class ViewController { labelSingular: objectMetadata.labelSingular, description: objectMetadata.description ?? undefined, icon: objectMetadata.icon ?? undefined, + isCustom: !belongsToTwentyStandardApp(objectMetadata), standardOverrides: objectMetadata.standardOverrides ?? undefined, }, 'labelPlural', diff --git a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts index 6a77cd3ffc1..7b148812486 100644 --- a/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/view/resolvers/view.resolver.ts @@ -74,6 +74,7 @@ export class ViewResolver { labelSingular: objectMetadata.labelSingular, description: objectMetadata.description ?? undefined, icon: objectMetadata.icon ?? undefined, + isCustom: objectMetadata.isCustom, standardOverrides: objectMetadata.standardOverrides ?? undefined, }, 'labelPlural', diff --git a/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/__tests__/enrich-command-menu-item-event-with-resolved-navigation.util.spec.ts b/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/__tests__/enrich-command-menu-item-event-with-resolved-navigation.util.spec.ts index b6817cade58..adc5017c396 100644 --- a/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/__tests__/enrich-command-menu-item-event-with-resolved-navigation.util.spec.ts +++ b/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/__tests__/enrich-command-menu-item-event-with-resolved-navigation.util.spec.ts @@ -11,6 +11,7 @@ import { } from 'src/engine/metadata-modules/flat-command-menu-item/utils/build-navigation-flat-command-menu-item.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; import { enrichCommandMenuItemEventWithResolvedNavigation } from 'src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util'; +import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications'; const mockI18nInstance = { _: (messageId: string) => messageId, @@ -26,6 +27,8 @@ const makeFlatObjectMetadata = ( universalIdentifier: 'obj-uid-1', workspaceId: 'ws-1', applicationId: 'app-1', + applicationUniversalIdentifier: + TWENTY_STANDARD_APPLICATION.universalIdentifier, labelPlural: 'People', labelSingular: 'Person', icon: 'IconUser', diff --git a/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util.ts b/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util.ts index 8ac3dd48bba..b3855eb40fc 100644 --- a/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util.ts +++ b/packages/twenty-server/src/engine/subscriptions/metadata-event/utils/enrich-command-menu-item-event-with-resolved-navigation.util.ts @@ -12,6 +12,7 @@ import { FlatCommandMenuItem } from 'src/engine/metadata-modules/flat-command-me import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type'; +import { belongsToTwentyStandardApp } from 'src/engine/metadata-modules/utils/belongs-to-twenty-standard-app.util'; type EnrichCommandMenuItemEventArgs = { record: FlatCommandMenuItem; @@ -48,7 +49,10 @@ export const enrichCommandMenuItemEventWithResolvedNavigation = ({ } const context = buildNavigationInterpolationContext({ - objectMetadata: flatObjectMetadata, + objectMetadata: { + ...flatObjectMetadata, + isCustom: !belongsToTwentyStandardApp(flatObjectMetadata), + }, locale, i18nInstance, });