Compare commits

...

1 Commits

Author SHA1 Message Date
Devessier effac1a391 feat: translate widget name 2026-03-02 15:24:54 +01:00
13 changed files with 356 additions and 8 deletions
@@ -24,8 +24,8 @@ import { TabListComponentInstanceContext } from '@/ui/layout/tab-list/states/con
import { type TabListProps } from '@/ui/layout/tab-list/types/TabListProps';
import { NodeDimension } from '@/ui/utilities/dimensions/components/NodeDimension';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useNavigatePageLayoutCommandMenu } from '@/command-menu/pages/page-layout/hooks/useNavigatePageLayoutCommandMenu';
import { PAGE_LAYOUT_TAB_LIST_DROPPABLE_IDS } from '@/page-layout/components/PageLayoutTabListDroppableIds';
@@ -46,7 +46,7 @@ import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/com
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { CommandMenuPages } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type PageLayoutType } from '~/generated-metadata/graphql';
import { PageLayoutType } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -100,7 +100,9 @@ export const PageLayoutTabList = ({
const { t } = useLingui();
const { isCustom } = useIsCurrentObjectCustom();
const shouldTranslateTabTitles = !isCustom;
const isRecordPageLayout = pageLayoutType === PageLayoutType.RECORD_PAGE;
const shouldTranslateTabTitles = isRecordPageLayout && !isCustom;
const tabsWithIcons: SingleTabProps[] = tabs.map((tab) => ({
id: tab.id,
@@ -0,0 +1,24 @@
import { msg, type MacroMessageDescriptor } from '@lingui/core/macro';
// Translation map for standard page layout widget titles.
// When the backend hasn't translated the widget title (e.g. when using
// frontend fallback layouts), this map is used to translate known
// standard widget titles at the rendering point.
// Custom/user-created widget titles that are not in this map will be
// displayed as-is.
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
export const STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS: Record<
string,
MacroMessageDescriptor
> = {
Fields: msg`Fields`,
Timeline: msg`Timeline`,
Tasks: msg`Tasks`,
Notes: msg`Notes`,
Files: msg`Files`,
Emails: msg`Emails`,
Calendar: msg`Calendar`,
Note: msg`Note`,
Task: msg`Task`,
Flow: msg`Flow`,
};
@@ -1,7 +1,9 @@
import { STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS } from '@/page-layout/constants/StandardPageLayoutWidgetTitleTranslations';
import { usePageLayoutContentContext } from '@/page-layout/contexts/PageLayoutContentContext';
import { useCurrentPageLayoutOrThrow } from '@/page-layout/hooks/useCurrentPageLayoutOrThrow';
import { useDeletePageLayoutWidget } from '@/page-layout/hooks/useDeletePageLayoutWidget';
import { useEditPageLayoutWidget } from '@/page-layout/hooks/useEditPageLayoutWidget';
import { useIsCurrentObjectCustom } from '@/page-layout/hooks/useIsCurrentObjectCustom';
import { isPageLayoutInEditModeComponentState } from '@/page-layout/states/isPageLayoutInEditModeComponentState';
import { pageLayoutDraggingWidgetIdComponentState } from '@/page-layout/states/pageLayoutDraggingWidgetIdComponentState';
import { pageLayoutEditingWidgetIdComponentState } from '@/page-layout/states/pageLayoutEditingWidgetIdComponentState';
@@ -26,6 +28,7 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { useSetAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentFamilyState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { type MouseEvent } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { IconLock } from 'twenty-ui/display';
@@ -47,8 +50,10 @@ type WidgetRendererProps = {
export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const theme = useTheme();
const { t } = useLingui();
const { deletePageLayoutWidget } = useDeletePageLayoutWidget();
const { handleEditWidget } = useEditPageLayoutWidget();
const { isCustom } = useIsCurrentObjectCustom();
const isPageLayoutInEditMode = useAtomComponentStateValue(
isPageLayoutInEditModeComponentState,
@@ -89,6 +94,9 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const isDeletingWidgetEnabled =
currentPageLayout.type !== PageLayoutType.RECORD_PAGE;
const isRecordPageLayout =
currentPageLayout.type === PageLayoutType.RECORD_PAGE;
// TODO: when we have more widgets without headers, we should use a more generic approach to hide the header
// each widget type could have metadata (e.g., hasHeader: boolean or headerMode: 'always' | 'editOnly' | 'never')
const isRichTextWidget = widget.type === WidgetType.STANDALONE_RICH_TEXT;
@@ -132,6 +140,15 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
const actions = useWidgetActions({ widget });
const shouldTranslateWidgetTitles = isRecordPageLayout && !isCustom;
// TODO: drop once the configuration of all record page layouts has been migrated to the backend.
const translatedWidgetTitle =
shouldTranslateWidgetTitles &&
STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS[widget.title]
? t(STANDARD_PAGE_LAYOUT_WIDGET_TITLE_TRANSLATIONS[widget.title])
: widget.title;
return (
<WidgetComponentInstanceContext.Provider value={{ instanceId: widget.id }}>
<WidgetCard
@@ -157,7 +174,7 @@ export const WidgetRenderer = ({ widget }: WidgetRendererProps) => {
isResizing={isResizing}
isReorderEnabled={isReorderEnabled}
isDeletingWidgetEnabled={isDeletingWidgetEnabled}
title={widget.title}
title={translatedWidgetTitle}
onRemove={handleRemove}
actions={actions}
forbiddenDisplay={
@@ -11,6 +11,7 @@ import { PageLayoutTabController } from 'src/engine/metadata-modules/page-layout
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
import { PageLayoutTabResolver } from 'src/engine/metadata-modules/page-layout-tab/resolvers/page-layout-tab.resolver';
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -31,6 +32,7 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
FlatPageLayoutWidgetModule,
ApplicationModule,
DashboardSyncModule,
PageLayoutModule,
],
controllers: [PageLayoutTabController],
providers: [
@@ -29,6 +29,7 @@ import { UpdatePageLayoutTabInput } from 'src/engine/metadata-modules/page-layou
import { PageLayoutTabDTO } from 'src/engine/metadata-modules/page-layout-tab/dtos/page-layout-tab.dto';
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -40,6 +41,7 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
export class PageLayoutTabResolver {
constructor(
private readonly pageLayoutTabService: PageLayoutTabService,
private readonly pageLayoutService: PageLayoutService,
private readonly i18nService: I18nService,
) {}
@@ -47,12 +49,19 @@ export class PageLayoutTabResolver {
async title(
@Parent() tab: PageLayoutTabDTO,
@Context() context: I18nContext,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const pageLayout = await this.pageLayoutService.findByIdOrThrow({
id: tab.pageLayoutId,
workspaceId: workspace.id,
});
return resolvePageLayoutTabTitle({
title: tab.title,
applicationId: tab.applicationId,
pageLayoutType: pageLayout.type,
i18nInstance: i18n,
});
}
@@ -2,6 +2,7 @@ import { type I18n } from '@lingui/core';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { resolvePageLayoutTabTitle } from 'src/engine/metadata-modules/page-layout-tab/utils/resolve-page-layout-tab-title.util';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
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');
@@ -27,6 +28,7 @@ describe('resolvePageLayoutTabTitle', () => {
const result = resolvePageLayoutTabTitle({
title: 'Home',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
@@ -42,6 +44,7 @@ describe('resolvePageLayoutTabTitle', () => {
const result = resolvePageLayoutTabTitle({
title: 'My Custom Tab',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
@@ -57,6 +60,7 @@ describe('resolvePageLayoutTabTitle', () => {
const result = resolvePageLayoutTabTitle({
title: '',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
@@ -84,6 +88,7 @@ describe('resolvePageLayoutTabTitle', () => {
const result = resolvePageLayoutTabTitle({
title: source,
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
@@ -100,6 +105,39 @@ describe('resolvePageLayoutTabTitle', () => {
const result = resolvePageLayoutTabTitle({
title: 'Home',
applicationId: customAppId,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
expect(result).toBe('Home');
});
it('should not translate title when pageLayoutType is DASHBOARD', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Accueil');
const result = resolvePageLayoutTabTitle({
title: 'Home',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.DASHBOARD,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
expect(result).toBe('Home');
});
it('should not translate title when pageLayoutType is RECORD_INDEX', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Accueil');
const result = resolvePageLayoutTabTitle({
title: 'Home',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_INDEX,
i18nInstance: mockI18n,
});
@@ -1,17 +1,24 @@
import { type I18n } from '@lingui/core';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const resolvePageLayoutTabTitle = ({
title,
applicationId,
pageLayoutType,
i18nInstance,
}: {
title: string;
applicationId: string;
pageLayoutType: PageLayoutType;
i18nInstance: I18n;
}): string => {
if (pageLayoutType !== PageLayoutType.RECORD_PAGE) {
return title;
}
if (applicationId !== TWENTY_STANDARD_APPLICATION.universalIdentifier) {
return title;
}
@@ -0,0 +1,18 @@
import { t } from '@lingui/core/macro';
// This file exists solely for Lingui string extraction.
// The strings defined here correspond to standard page layout widget titles
// so they appear in the .po catalogs and can be translated at resolve time
// via generateMessageId hash lookups.
export const getStandardPageLayoutWidgetTitles = () => [
t`Fields`,
t`Timeline`,
t`Tasks`,
t`Notes`,
t`Files`,
t`Emails`,
t`Calendar`,
t`Note`,
t`Task`,
t`Flow`,
];
@@ -3,8 +3,8 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import GraphQLJSON from 'graphql-type-json';
import {
PageLayoutWidgetConditionalDisplay,
PageLayoutWidgetPosition,
PageLayoutWidgetConditionalDisplay,
PageLayoutWidgetPosition,
} from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -35,6 +35,9 @@ export class PageLayoutWidgetDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field(() => UUIDScalarType, { nullable: false })
applicationId: string;
@Field(() => UUIDScalarType, { nullable: false })
pageLayoutTabId: string;
@@ -6,10 +6,12 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
import { FlatPageLayoutWidgetModule } from 'src/engine/metadata-modules/flat-page-layout-widget/flat-page-layout-widget.module';
import { PageLayoutTabModule } from 'src/engine/metadata-modules/page-layout-tab/page-layout-tab.module';
import { PageLayoutWidgetController } from 'src/engine/metadata-modules/page-layout-widget/controllers/page-layout-widget.controller';
import { PageLayoutWidgetEntity } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
import { PageLayoutWidgetResolver } from 'src/engine/metadata-modules/page-layout-widget/resolvers/page-layout-widget.resolver';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
import { PageLayoutModule } from 'src/engine/metadata-modules/page-layout/page-layout.module';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
@@ -29,6 +31,8 @@ import { DashboardSyncModule } from 'src/modules/dashboard-sync/dashboard-sync.m
FlatPageLayoutWidgetModule,
ApplicationModule,
DashboardSyncModule,
PageLayoutModule,
PageLayoutTabModule,
],
controllers: [PageLayoutWidgetController],
providers: [
@@ -4,22 +4,34 @@ import {
UseInterceptors,
UsePipes,
} from '@nestjs/common';
import { Args, Mutation, Parent, Query, ResolveField } from '@nestjs/graphql';
import {
Args,
Context,
Mutation,
Parent,
Query,
ResolveField,
} from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
import { CreatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/create-page-layout-widget.input';
import { UpdatePageLayoutWidgetInput } from 'src/engine/metadata-modules/page-layout-widget/dtos/inputs/update-page-layout-widget.input';
import { PageLayoutWidgetDTO } from 'src/engine/metadata-modules/page-layout-widget/dtos/page-layout-widget.dto';
import { WidgetConfiguration } from 'src/engine/metadata-modules/page-layout-widget/dtos/widget-configuration.interface';
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
import { resolvePageLayoutWidgetTitle } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-page-layout-widget-title.util';
import { PageLayoutService } from 'src/engine/metadata-modules/page-layout/services/page-layout.service';
import { PageLayoutGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/page-layout/utils/page-layout-graphql-api-exception.filter';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@@ -31,8 +43,37 @@ import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/wor
export class PageLayoutWidgetResolver {
constructor(
private readonly pageLayoutWidgetService: PageLayoutWidgetService,
private readonly pageLayoutTabService: PageLayoutTabService,
private readonly pageLayoutService: PageLayoutService,
private readonly i18nService: I18nService,
) {}
@ResolveField(() => String)
async title(
@Parent() widget: PageLayoutWidgetDTO,
@Context() context: I18nContext,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const tab = await this.pageLayoutTabService.findByIdOrThrow({
id: widget.pageLayoutTabId,
workspaceId: workspace.id,
});
const pageLayout = await this.pageLayoutService.findByIdOrThrow({
id: tab.pageLayoutId,
workspaceId: workspace.id,
});
return resolvePageLayoutWidgetTitle({
title: widget.title,
applicationId: widget.applicationId,
pageLayoutType: pageLayout.type,
i18nInstance: i18n,
});
}
@Query(() => [PageLayoutWidgetDTO])
@UseGuards(NoPermissionGuard)
async getPageLayoutWidgets(
@@ -0,0 +1,149 @@
import { type I18n } from '@lingui/core';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { resolvePageLayoutWidgetTitle } from 'src/engine/metadata-modules/page-layout-widget/utils/resolve-page-layout-widget-title.util';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
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');
const mockGenerateMessageId = generateMessageId as jest.MockedFunction<
typeof generateMessageId
>;
describe('resolvePageLayoutWidgetTitle', () => {
let mockI18n: jest.Mocked<I18n>;
beforeEach(() => {
jest.clearAllMocks();
mockI18n = {
_: jest.fn(),
} as unknown as jest.Mocked<I18n>;
});
it('should return translated title when catalog has a match', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Champs');
const result = resolvePageLayoutWidgetTitle({
title: 'Fields',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).toHaveBeenCalledWith('Fields');
expect(mockI18n._).toHaveBeenCalledWith('abc123');
expect(result).toBe('Champs');
});
it('should return original title when catalog returns the hash (no translation found)', () => {
mockGenerateMessageId.mockReturnValue('xyz789');
mockI18n._.mockReturnValue('xyz789');
const result = resolvePageLayoutWidgetTitle({
title: 'My Custom Widget',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).toHaveBeenCalledWith('My Custom Widget');
expect(mockI18n._).toHaveBeenCalledWith('xyz789');
expect(result).toBe('My Custom Widget');
});
it('should return original title for empty string', () => {
mockGenerateMessageId.mockReturnValue('empty-hash');
mockI18n._.mockReturnValue('empty-hash');
const result = resolvePageLayoutWidgetTitle({
title: '',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(result).toBe('');
});
it('should translate standard widget titles', () => {
const standardWidgets = [
{ source: 'Fields', translated: 'Champs' },
{ 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: 'Task', translated: 'Tâche' },
{ source: 'Flow', translated: 'Flux' },
];
standardWidgets.forEach(({ source, translated }) => {
jest.clearAllMocks();
mockGenerateMessageId.mockReturnValue(`hash-${source}`);
mockI18n._.mockReturnValue(translated);
const result = resolvePageLayoutWidgetTitle({
title: source,
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(result).toBe(translated);
});
});
it('should not translate title when applicationId is not from standard app', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Champs');
const customAppId = '11111111-1111-1111-1111-111111111111';
const result = resolvePageLayoutWidgetTitle({
title: 'Fields',
applicationId: customAppId,
pageLayoutType: PageLayoutType.RECORD_PAGE,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
expect(result).toBe('Fields');
});
it('should not translate title when pageLayoutType is DASHBOARD', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Champs');
const result = resolvePageLayoutWidgetTitle({
title: 'Fields',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.DASHBOARD,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
expect(result).toBe('Fields');
});
it('should not translate title when pageLayoutType is RECORD_INDEX', () => {
mockGenerateMessageId.mockReturnValue('abc123');
mockI18n._.mockReturnValue('Champs');
const result = resolvePageLayoutWidgetTitle({
title: 'Fields',
applicationId: TWENTY_STANDARD_APPLICATION.universalIdentifier,
pageLayoutType: PageLayoutType.RECORD_INDEX,
i18nInstance: mockI18n,
});
expect(mockGenerateMessageId).not.toHaveBeenCalled();
expect(mockI18n._).not.toHaveBeenCalled();
expect(result).toBe('Fields');
});
});
@@ -0,0 +1,34 @@
import { type I18n } from '@lingui/core';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { PageLayoutType } from 'src/engine/metadata-modules/page-layout/enums/page-layout-type.enum';
import { TWENTY_STANDARD_APPLICATION } from 'src/engine/workspace-manager/twenty-standard-application/constants/twenty-standard-applications';
export const resolvePageLayoutWidgetTitle = ({
title,
applicationId,
pageLayoutType,
i18nInstance,
}: {
title: string;
applicationId: string;
pageLayoutType: PageLayoutType;
i18nInstance: I18n;
}): string => {
if (pageLayoutType !== PageLayoutType.RECORD_PAGE) {
return title;
}
if (applicationId !== TWENTY_STANDARD_APPLICATION.universalIdentifier) {
return title;
}
const messageId = generateMessageId(title);
const translatedMessage = i18nInstance._(messageId);
if (translatedMessage === messageId) {
return title;
}
return translatedMessage;
};