fix: standard object metadata override (#13215)

# Issue

- fix #13156, related #13105

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Nabhag Motivaras
2025-07-16 12:20:14 +05:30
committed by GitHub
parent 87e494d85f
commit ffcbfa6215
15 changed files with 645 additions and 86 deletions

View File

@ -81,7 +81,7 @@ export class FieldMetadataResolver {
try {
const updatedInput = (await this.beforeUpdateOneField.run(input, {
workspaceId,
locale: context.req.headers['x-locale'],
locale: context.req.locale,
})) as UpdateOneFieldMetadataInput;
return await this.fieldMetadataService.updateOne(updatedInput.id, {

View File

@ -2,7 +2,7 @@ import { i18n } from '@lingui/core';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { resolveOverridableString } from 'src/engine/metadata-modules/field-metadata/utils/resolve-overridable-string.util';
import { resolveFieldMetadataStandardOverride } from 'src/engine/metadata-modules/field-metadata/utils/resolve-field-metadata-standard-override.util';
jest.mock('@lingui/core');
jest.mock('src/engine/core-modules/i18n/utils/generateMessageId');
@ -12,7 +12,7 @@ const mockGenerateMessageId = generateMessageId as jest.MockedFunction<
typeof generateMessageId
>;
describe('resolveOverridableString', () => {
describe('resolveFieldMetadataStandardOverride', () => {
beforeEach(() => {
jest.clearAllMocks();
});
@ -27,7 +27,11 @@ describe('resolveOverridableString', () => {
standardOverrides: undefined,
};
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(result).toBe('Custom Label');
});
@ -41,7 +45,7 @@ describe('resolveOverridableString', () => {
standardOverrides: undefined,
};
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'description',
undefined,
@ -59,7 +63,7 @@ describe('resolveOverridableString', () => {
standardOverrides: undefined,
};
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'icon',
SOURCE_LOCALE,
@ -81,7 +85,11 @@ describe('resolveOverridableString', () => {
},
};
const result = resolveOverridableString(fieldMetadata, 'icon', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'icon',
'fr-FR',
);
expect(result).toBe('override-icon');
});
@ -104,11 +112,15 @@ describe('resolveOverridableString', () => {
},
};
expect(resolveOverridableString(fieldMetadata, 'label', 'fr-FR')).toBe(
'Libellé traduit',
);
expect(
resolveOverridableString(fieldMetadata, 'description', 'fr-FR'),
resolveFieldMetadataStandardOverride(fieldMetadata, 'label', 'fr-FR'),
).toBe('Libellé traduit');
expect(
resolveFieldMetadataStandardOverride(
fieldMetadata,
'description',
'fr-FR',
),
).toBe('Description traduite');
});
@ -130,7 +142,11 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
@ -153,7 +169,7 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'description',
'fr-FR',
@ -180,7 +196,7 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
undefined,
@ -205,13 +221,25 @@ describe('resolveOverridableString', () => {
};
expect(
resolveOverridableString(fieldMetadata, 'label', SOURCE_LOCALE),
resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
SOURCE_LOCALE,
),
).toBe('Overridden Label');
expect(
resolveOverridableString(fieldMetadata, 'description', SOURCE_LOCALE),
resolveFieldMetadataStandardOverride(
fieldMetadata,
'description',
SOURCE_LOCALE,
),
).toBe('Overridden Description');
expect(
resolveOverridableString(fieldMetadata, 'icon', SOURCE_LOCALE),
resolveFieldMetadataStandardOverride(
fieldMetadata,
'icon',
SOURCE_LOCALE,
),
).toBe('overridden-icon');
});
@ -229,7 +257,11 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
@ -248,7 +280,7 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
SOURCE_LOCALE,
@ -271,7 +303,7 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
SOURCE_LOCALE,
@ -294,7 +326,11 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('standard.label.message.id');
mockI18n._.mockReturnValue('Libellé traduit automatiquement');
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
expect(mockI18n._).toHaveBeenCalledWith('standard.label.message.id');
@ -315,7 +351,11 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue(messageId);
mockI18n._.mockReturnValue(messageId);
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
@ -338,7 +378,11 @@ describe('resolveOverridableString', () => {
},
};
const result = resolveOverridableString(fieldMetadata, 'label', 'fr-FR');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'fr-FR',
);
expect(result).toBe('Translation Override');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
@ -356,7 +400,7 @@ describe('resolveOverridableString', () => {
},
};
const result = resolveOverridableString(
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
SOURCE_LOCALE,
@ -379,7 +423,11 @@ describe('resolveOverridableString', () => {
mockGenerateMessageId.mockReturnValue('auto.translation.id');
mockI18n._.mockReturnValue('Auto Translated Label');
const result = resolveOverridableString(fieldMetadata, 'label', 'de-DE');
const result = resolveFieldMetadataStandardOverride(
fieldMetadata,
'label',
'de-DE',
);
expect(result).toBe('Auto Translated Label');
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');

View File

@ -6,7 +6,7 @@ import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
export const resolveOverridableString = (
export const resolveFieldMetadataStandardOverride = (
fieldMetadata: Pick<
FieldMetadataDTO,
'label' | 'description' | 'icon' | 'isCustom' | 'standardOverrides'

View File

@ -27,6 +27,7 @@ import {
import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { objectMetadataGraphqlApiExceptionHandler } from 'src/engine/metadata-modules/object-metadata/utils/object-metadata-graphql-api-exception-handler.util';
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
@ -48,10 +49,10 @@ export class ObjectMetadataResolver {
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.objectMetadataService.resolveOverridableString(
return resolveObjectMetadataStandardOverride(
objectMetadata,
'labelPlural',
context.req.headers['x-locale'],
context.req.locale,
);
}
@ -60,10 +61,10 @@ export class ObjectMetadataResolver {
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.objectMetadataService.resolveOverridableString(
return resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
context.req.headers['x-locale'],
context.req.locale,
);
}
@ -72,10 +73,10 @@ export class ObjectMetadataResolver {
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.objectMetadataService.resolveOverridableString(
return resolveObjectMetadataStandardOverride(
objectMetadata,
'description',
context.req.headers['x-locale'],
context.req.locale,
);
}
@ -85,10 +86,10 @@ export class ObjectMetadataResolver {
@Parent() objectMetadata: ObjectMetadataDTO,
@Context() context: I18nContext,
): Promise<string> {
return this.objectMetadataService.resolveOverridableString(
return resolveObjectMetadataStandardOverride(
objectMetadata,
'icon',
context.req.headers['x-locale'],
context.req.locale,
);
}
@ -118,7 +119,7 @@ export class ObjectMetadataResolver {
try {
const updatedInput = (await this.beforeUpdateOneObject.run(input, {
workspaceId,
locale: context.req.headers['x-locale'],
locale: context.req.locale,
})) as UpdateOneObjectInput;
return await this.objectMetadataService.updateOneObject(
@ -141,7 +142,7 @@ export class ObjectMetadataResolver {
{
objectMetadata,
workspaceId: workspace.id,
locale: context.req.headers['x-locale'],
locale: context.req.locale,
},
);

View File

@ -1,10 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { i18n } from '@lingui/core';
import { Query, QueryOptions } from '@ptc-org/nestjs-query-core';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { APP_LOCALES } from 'twenty-shared/translations';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
FindManyOptions,
@ -14,12 +12,10 @@ import {
Repository,
} from 'typeorm';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service';
import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input';
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
import {
UpdateObjectPayload,
UpdateOneObjectInput,
@ -405,6 +401,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
const formattedUpdatedObject = {
...updatedObject,
createdAt: new Date(updatedObject.createdAt),
updatedAt: new Date(updatedObject.updatedAt),
};
return formattedUpdatedObject;
@ -636,31 +633,4 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
didUpdateLabelOrIcon: false,
};
}
async resolveOverridableString(
objectMetadata: ObjectMetadataDTO,
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
locale: keyof typeof APP_LOCALES | undefined,
): Promise<string> {
if (objectMetadata.isCustom) {
return objectMetadata[labelKey];
}
const translationValue =
// @ts-expect-error legacy noImplicitAny
objectMetadata.standardOverrides?.translations?.[locale]?.[labelKey];
if (isDefined(translationValue)) {
return translationValue;
}
const messageId = generateMessageId(objectMetadata[labelKey] ?? '');
const translatedMessage = i18n._(messageId);
if (translatedMessage === messageId) {
return objectMetadata[labelKey] ?? '';
}
return translatedMessage;
}
}

View File

@ -0,0 +1,481 @@
import { i18n } from '@lingui/core';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { 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('@lingui/core');
jest.mock('src/engine/core-modules/i18n/utils/generateMessageId');
const mockI18n = i18n as jest.Mocked<typeof i18n>;
const mockGenerateMessageId = generateMessageId as jest.MockedFunction<
typeof generateMessageId
>;
describe('resolveObjectMetadataStandardOverride', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Custom objects', () => {
it('should return the object value for custom labelSingular object', () => {
const objectMetadata = {
labelSingular: 'My Custom',
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'labelPlural'
| 'labelSingular'
| 'description'
| 'icon'
| 'isCustom'
| 'standardOverrides'
>;
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(result).toBe('My Custom');
});
it('should return the object value for custom description object', () => {
const objectMetadata = {
labelSingular: 'My Custom',
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'labelPlural'
| 'labelSingular'
| 'description'
| 'icon'
| 'isCustom'
| 'standardOverrides'
>;
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'description',
undefined,
);
expect(result).toBe('Custom Description');
});
it('should return the object value for custom icon object', () => {
const objectMetadata = {
labelSingular: 'My Custom',
labelPlural: 'My Customs',
description: 'Custom Description',
icon: 'custom-icon',
isCustom: true,
standardOverrides: undefined,
} satisfies Pick<
ObjectMetadataDTO,
| 'labelPlural'
| 'labelSingular'
| 'description'
| 'icon'
| 'isCustom'
| 'standardOverrides'
>;
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'icon',
SOURCE_LOCALE,
);
expect(result).toBe('custom-icon');
});
});
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 result = resolveObjectMetadataStandardOverride(
objectMetadata,
'icon',
'fr-FR',
);
expect(result).toBe('override-icon');
});
});
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,
standardOverrides: {
translations: {
'fr-FR': {
labelSingular: 'Libellé traduit',
labelPlural: 'Libellés traduits',
description: 'Description traduite',
},
},
},
};
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
),
).toBe('Libellé traduit');
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'labelPlural',
'fr-FR',
),
).toBe('Libellés traduits');
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'description',
'fr-FR',
),
).toBe('Description traduite');
});
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,
standardOverrides: {
translations: {
'es-ES': {
labelSingular: 'Etiqueta en español',
labelPlural: 'Etiquetas en español',
description: 'Descripción en español',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
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,
standardOverrides: {
translations: {
'fr-FR': {
labelPlural: 'Libellés traduits',
labelSingular: 'Libellé traduit',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'description',
'fr-FR',
);
expect(result).toBe('Standard Description');
});
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,
standardOverrides: {
translations: {
'fr-FR': {
labelSingular: 'Libellé traduit',
labelPlural: 'Libellés traduits',
description: 'Description traduite',
},
},
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
undefined,
);
expect(result).toBe('Standard Label');
});
});
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,
standardOverrides: {
labelSingular: 'Overridden Label',
labelPlural: 'Overridden Labels',
description: 'Overridden Description',
icon: 'overridden-icon',
},
};
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
SOURCE_LOCALE,
),
).toBe('Overridden Label');
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'labelPlural',
SOURCE_LOCALE,
),
).toBe('Overridden Labels');
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'description',
SOURCE_LOCALE,
),
).toBe('Overridden Description');
expect(
resolveObjectMetadataStandardOverride(
objectMetadata,
'icon',
SOURCE_LOCALE,
),
).toBe('overridden-icon');
});
it('should not use direct override for non-SOURCE_LOCALE', () => {
const objectMetadata = {
labelSingular: 'Standard Label',
labelPlural: 'Standard Labels',
description: 'Standard Description',
icon: 'default-icon',
isCustom: false,
standardOverrides: {
labelSingular: 'Overridden Label',
labelPlural: 'Overridden Labels',
},
};
mockGenerateMessageId.mockReturnValue('generated-message-id');
mockI18n._.mockReturnValue('generated-message-id');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
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,
},
};
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
SOURCE_LOCALE,
);
expect(result).toBe('Standard Label');
});
});
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');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
expect(mockI18n._).toHaveBeenCalledWith('standard.label.message.id');
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);
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(result).toBe('Standard Label');
});
});
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,
standardOverrides: {
labelSingular: 'Source Override',
labelPlural: 'Source Overrides',
translations: {
'fr-FR': {
labelSingular: 'Translation Override',
labelPlural: 'Translation Overrides',
},
},
},
};
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'fr-FR',
);
expect(result).toBe('Translation Override');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).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,
standardOverrides: {
labelSingular: 'Source Override',
labelPlural: 'Source Overrides',
},
};
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
SOURCE_LOCALE,
);
expect(result).toBe('Source Override');
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).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');
const result = resolveObjectMetadataStandardOverride(
objectMetadata,
'labelSingular',
'de-DE',
);
expect(result).toBe('Auto Translated Label');
expect(mockGenerateMessageId).toHaveBeenCalledWith('Standard Label');
expect(mockI18n._).toHaveBeenCalledWith('auto.translation.id');
});
});
});

View File

@ -0,0 +1,61 @@
import { i18n } from '@lingui/core';
import { isNonEmptyString } from '@sniptt/guards';
import { 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 { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
export const resolveObjectMetadataStandardOverride = (
objectMetadata: Pick<
ObjectMetadataDTO,
| 'labelPlural'
| 'labelSingular'
| 'description'
| 'icon'
| 'isCustom'
| 'standardOverrides'
>,
labelKey: 'labelPlural' | 'labelSingular' | 'description' | 'icon',
locale: keyof typeof APP_LOCALES | undefined,
): string => {
if (objectMetadata.isCustom) {
return objectMetadata[labelKey] ?? '';
}
if (
labelKey === 'icon' &&
isDefined(objectMetadata.standardOverrides?.icon)
) {
return objectMetadata.standardOverrides.icon;
}
if (
isDefined(objectMetadata.standardOverrides?.translations) &&
isDefined(locale) &&
labelKey !== 'icon'
) {
const translationValue =
objectMetadata.standardOverrides.translations[locale]?.[labelKey];
if (isDefined(translationValue)) {
return translationValue;
}
}
if (
locale === SOURCE_LOCALE &&
isNonEmptyString(objectMetadata.standardOverrides?.[labelKey])
) {
return objectMetadata.standardOverrides[labelKey] ?? '';
}
const messageId = generateMessageId(objectMetadata[labelKey] ?? '');
const translatedMessage = i18n._(messageId);
if (translatedMessage === messageId) {
return objectMetadata[labelKey] ?? '';
}
return translatedMessage;
};