Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b93c277bd |
@@ -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 {
|
||||
|
||||
+15
@@ -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);
|
||||
};
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
export const standardMetadataDescriptorByMessage = new Map<
|
||||
string,
|
||||
MessageDescriptor
|
||||
>();
|
||||
+28
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
+81
-177
@@ -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> = {}): FieldFixture => ({
|
||||
label: 'Standard Label',
|
||||
description: 'Standard Description',
|
||||
icon: 'default-icon',
|
||||
isCustom: false,
|
||||
standardOverrides: undefined,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('resolveFieldMetadataStandardOverride', () => {
|
||||
let mockI18n: jest.Mocked<I18n>;
|
||||
|
||||
@@ -18,17 +39,15 @@ describe('resolveFieldMetadataStandardOverride', () => {
|
||||
mockI18n = {
|
||||
_: jest.fn(),
|
||||
} as unknown as jest.Mocked<I18n>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+12
-23
@@ -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;
|
||||
};
|
||||
|
||||
+94
-256
@@ -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> = {},
|
||||
): 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<I18n>;
|
||||
|
||||
@@ -19,28 +49,15 @@ describe('resolveObjectMetadataStandardOverride', () => {
|
||||
mockI18n = {
|
||||
_: jest.fn(),
|
||||
} as unknown as jest.Mocked<I18n>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-25
@@ -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;
|
||||
};
|
||||
|
||||
+18
-15
@@ -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);
|
||||
}
|
||||
|
||||
+21
-51
@@ -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<I18n>;
|
||||
@@ -18,11 +24,11 @@ describe('resolvePageLayoutTabTitle', () => {
|
||||
mockI18n = {
|
||||
_: jest.fn(),
|
||||
} as unknown as jest.Mocked<I18n>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
+2
-9
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+7
-2
@@ -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 ?? '';
|
||||
};
|
||||
|
||||
+31
@@ -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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user