fix: standard object metadata override (#13215)
# Issue - fix #13156, related #13105 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
3
packages/twenty-server/@types/express.d.ts
vendored
3
packages/twenty-server/@types/express.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@ -9,6 +11,7 @@ declare module 'express-serve-static-core' {
|
||||
user?: User | null;
|
||||
apiKey?: ApiKey | null;
|
||||
userWorkspace?: UserWorkspace;
|
||||
locale: keyof typeof APP_LOCALES;
|
||||
workspace?: Workspace;
|
||||
workspaceId?: string;
|
||||
workspaceMetadataVersion?: number;
|
||||
|
||||
@ -18,10 +18,7 @@ export function useCachedMetadata(config: CacheMetadataPluginConfig): Plugin {
|
||||
const workspaceMetadataVersion =
|
||||
serverContext.req.workspaceMetadataVersion ?? '0';
|
||||
const operationName = getOperationName(serverContext);
|
||||
const locale =
|
||||
serverContext.req.userWorkspace?.locale ??
|
||||
serverContext.req.headers['x-locale'] ??
|
||||
'';
|
||||
const locale = serverContext.req.locale;
|
||||
const localeCacheKey = isNonEmptyString(locale) ? `:${locale}` : '';
|
||||
const queryHash = createHash('sha256')
|
||||
.update(serverContext.req.body.query)
|
||||
|
||||
@ -519,7 +519,7 @@ export class AuthResolver {
|
||||
return await this.resetPasswordService.sendEmailPasswordResetLink(
|
||||
resetToken,
|
||||
emailPasswordResetInput.email,
|
||||
context.req.headers['x-locale'] || SOURCE_LOCALE,
|
||||
context.req.locale,
|
||||
);
|
||||
}
|
||||
|
||||
@ -535,11 +535,7 @@ export class AuthResolver {
|
||||
passwordResetToken,
|
||||
);
|
||||
|
||||
await this.authService.updatePassword(
|
||||
id,
|
||||
newPassword,
|
||||
context.req.headers['x-locale'] || SOURCE_LOCALE,
|
||||
);
|
||||
await this.authService.updatePassword(id, newPassword, context.req.locale);
|
||||
|
||||
return await this.resetPasswordService.invalidatePasswordResetToken(id);
|
||||
}
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input';
|
||||
import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output';
|
||||
@ -41,7 +39,7 @@ export class EmailVerificationResolver {
|
||||
return await this.emailVerificationService.resendEmailVerificationToken(
|
||||
resendEmailVerificationTokenInput.email,
|
||||
workspace,
|
||||
context.req.headers['x-locale'] ?? SOURCE_LOCALE,
|
||||
context.req.locale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
@Injectable()
|
||||
export class I18nMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
const locale = req.headers['x-locale'] as keyof typeof APP_LOCALES;
|
||||
const locale = req.locale;
|
||||
|
||||
if (locale && Object.values(APP_LOCALES).includes(locale)) {
|
||||
i18n.activate(locale);
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
export type I18nContext = {
|
||||
req: {
|
||||
headers: {
|
||||
'x-locale': (typeof APP_LOCALES)[keyof typeof APP_LOCALES] | undefined;
|
||||
};
|
||||
locale: keyof typeof APP_LOCALES;
|
||||
};
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ import { IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { FieldMetadataRelationService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-relation.service';
|
||||
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';
|
||||
import { IndexFieldMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-field-metadata.dto';
|
||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@ -177,7 +177,7 @@ export class DataloaderService {
|
||||
>(
|
||||
(acc, field) => ({
|
||||
...acc,
|
||||
[field]: resolveOverridableString(
|
||||
[field]: resolveFieldMetadataStandardOverride(
|
||||
fieldMetadata,
|
||||
field,
|
||||
dataLoaderParams[0].locale,
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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');
|
||||
@ -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'
|
||||
@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
@ -158,6 +159,11 @@ export class MiddlewareService {
|
||||
request.workspaceMemberId = data.workspaceMemberId;
|
||||
request.userWorkspaceId = data.userWorkspaceId;
|
||||
request.authProvider = data.authProvider;
|
||||
|
||||
request.locale =
|
||||
((data.userWorkspace?.locale ??
|
||||
request.headers['x-locale']) as keyof typeof APP_LOCALES) ??
|
||||
SOURCE_LOCALE;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
Reference in New Issue
Block a user