feat: add lingui/no-unlocalized-strings ESLint rule and fix translations (#16610)

## Summary
This PR adds the `lingui/no-unlocalized-strings` ESLint rule to detect
untranslated strings and fixes translation issues across multiple
components.

## Changes

### ESLint Configuration (`eslint.config.react.mjs`)
- Added comprehensive `ignore` patterns for non-translatable strings
(CSS values, HTML attributes, technical identifiers)
- Added `ignoreNames` for props that don't need translation (className,
data-*, aria-*, etc.)
- Added `ignoreFunctions` for console methods, URL APIs, and other
non-user-facing functions
- Disabled rule for debug files, storybook, and test files

### Components Fixed (~19 files)
- Object record components (field inputs, pickers, merge dialogs)
- Settings components (accounts, admin panel)
- Serverless function components
- Record table and title cell components

## Status
🚧 **Work in Progress** - ~124 files remaining to fix

This PR is being submitted as draft to allow progressive fixing of
remaining translation issues.

## Testing
- Run `npx eslint "src/**/*.tsx"` in `packages/twenty-front` to check
remaining issues
This commit is contained in:
Félix Malfait
2025-12-17 22:08:33 +01:00
committed by GitHub
parent c13b955a36
commit 1088f7bbab
368 changed files with 56823 additions and 836 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,5 @@
import { type ReactNode, useContext } from 'react';
import { t } from '@lingui/core/macro';
import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay';
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
@@ -25,7 +26,7 @@ export const ActionModal = ({
title,
subtitle,
onConfirmClick,
confirmButtonText = 'Confirm',
confirmButtonText = t`Confirm`,
confirmButtonAccent = 'danger',
isLoading = false,
closeSidePanelOnShowPageOptionsActionExecution,
@@ -46,8 +46,5 @@ export const ActionDisplay = ({
return <ActionDropdownItem action={action} onClick={onClick} to={to} />;
}
return assertUnreachable(
displayType,
`Unsupported display type: ${displayType}`,
);
return assertUnreachable(displayType, 'Unsupported display type');
};
@@ -17,6 +17,7 @@ import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { IconLayoutSidebarRightExpand } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
@@ -32,6 +33,7 @@ const StyledDropdownMenuContainer = styled.div`
`;
export const RecordIndexActionMenuDropdown = () => {
const { t } = useLingui();
const { actions } = useContext(ActionMenuContext);
const recordIndexActions = actions.filter(
@@ -103,7 +105,7 @@ export const RecordIndexActionMenuDropdown = () => {
openCommandMenu();
}}
focused={selectedItemId === 'more-actions'}
text="More actions"
text={t`More actions`}
/>
</SelectableListItem>
</SelectableList>
@@ -1,5 +1,6 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { CalendarEventParticipantsResponseStatus } from '@/activities/calendar/components/CalendarEventParticipantsResponseStatus';
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
@@ -77,6 +78,7 @@ const StyledPropertyBox = styled(PropertyBox)`
export const CalendarEventDetails = ({
calendarEvent,
}: CalendarEventDetailsProps) => {
const { t } = useLingui();
const theme = useTheme();
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.CalendarEvent,
@@ -155,14 +157,14 @@ export const CalendarEventDetails = ({
variant={ChipVariant.Highlighted}
clickable={false}
leftComponent={<IconCalendarEvent size={theme.icon.size.md} />}
label="Event"
label={t`Event`}
/>
<StyledHeader>
<StyledTitle canceled={calendarEvent.isCanceled}>
{calendarEvent.title}
</StyledTitle>
<StyledCreatedAt>
Created{' '}
{t`Created`}{' '}
{beautifyPastDateRelativeToNow(
new Date(calendarEvent.externalCreatedAt),
)}
@@ -22,7 +22,7 @@ export const CalendarEventParticipantsResponseStatus = ({
}
});
const responseStatusOrder: ('Yes' | 'Maybe' | 'No')[] = [
const responseStatusOrder: Array<'Yes' | 'Maybe' | 'No'> = [
'Yes',
'Maybe',
'No',
@@ -1,5 +1,6 @@
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { format } from 'date-fns';
import { useRecoilValue } from 'recoil';
@@ -93,7 +94,7 @@ export const CalendarEventRow = ({
const hasEnded = hasCalendarEventEnded(calendarEvent);
const startTimeLabel = calendarEvent.isFullDay
? 'All day'
? t`All day`
: format(startsAt, 'HH:mm');
const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { format, getYear } from 'date-fns';
import { useRecoilValue } from 'recoil';
@@ -46,6 +47,7 @@ const StyledTitleContainer = styled.div`
`;
export const CalendarEventsCard = () => {
const { t } = useLingui();
const targetRecord = useTargetRecord();
const { localeCatalog } = useRecoilValue(dateLocaleState);
@@ -95,6 +97,8 @@ export const CalendarEventsCard = () => {
}
};
const objectName = targetRecord.targetObjectNameSingular;
if (firstQueryLoading) {
return <SkeletonLoader />;
}
@@ -109,11 +113,10 @@ export const CalendarEventsCard = () => {
<AnimatedPlaceholder type="noMatchRecord" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No Events
{t`No Events`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
No events have been scheduled with this{' '}
{targetRecord.targetObjectNameSingular} yet.
{t`No events have been scheduled with this ${objectName} yet.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</AnimatedPlaceholderEmptyContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useInView } from 'react-intersection-observer';
type CustomResolverFetchMoreLoaderProps = {
@@ -34,7 +35,7 @@ export const CustomResolverFetchMoreLoader = ({
return (
<StyledContainer ref={tbodyRef}>
{loading && <StyledText>Loading more...</StyledText>}
{loading && <StyledText>{t`Loading more...`}</StyledText>}
</StyledContainer>
);
};
@@ -1,3 +1,4 @@
import { t } from '@lingui/core/macro';
import {
AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer,
@@ -11,7 +12,7 @@ export const EmailLoader = ({ loadingText }: { loadingText?: string }) => (
<AnimatedPlaceholder type="loadingMessages" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
{loadingText || 'Loading emails'}
{loadingText || t`Loading emails`}
</AnimatedPlaceholderEmptyTitle>
<Loader />
</AnimatedPlaceholderEmptyTextContainer>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { AppTooltip, IconLock, TooltipDelay } from 'twenty-ui/display';
import { MessageChannelVisibility } from '~/generated/graphql';
@@ -31,6 +32,7 @@ type EmailThreadNotSharedProps = {
export const EmailThreadNotShared = ({
visibility,
}: EmailThreadNotSharedProps) => {
const { t } = useLingui();
const theme = useTheme();
const containerId = 'email-thread-not-shared';
const isCompact = visibility === MessageChannelVisibility.SUBJECT;
@@ -39,12 +41,12 @@ export const EmailThreadNotShared = ({
<>
<StyledContainer id={containerId} isCompact={isCompact}>
<IconLock size={theme.icon.size.sm} />
{'Not shared'}
{t`Not shared`}
</StyledContainer>
{visibility === MessageChannelVisibility.SUBJECT && (
<AppTooltip
anchorSelect={`#${containerId}`}
content="Only the subject is shared"
content={t`Only the subject is shared`}
delay={TooltipDelay.mediumDelay}
noArrow
place="bottom"
@@ -3,6 +3,7 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useLingui } from '@lingui/react/macro';
import {
IconDotsVertical,
IconDownload,
@@ -27,6 +28,7 @@ export const AttachmentDropdown = ({
attachmentId,
hasDownloadPermission,
}: AttachmentDropdownProps) => {
const { t } = useLingui();
const dropdownId = `${attachmentId}-attachment-dropdown`;
const { closeDropdown } = useCloseDropdown();
@@ -57,18 +59,18 @@ export const AttachmentDropdown = ({
<DropdownMenuItemsContainer>
{hasDownloadPermission && (
<MenuItem
text="Download"
text={t`Download`}
LeftIcon={IconDownload}
onClick={handleDownload}
/>
)}
<MenuItem
text="Rename"
text={t`Rename`}
LeftIcon={IconPencil}
onClick={handleRename}
/>
<MenuItem
text="Delete"
text={t`Delete`}
accent="danger"
LeftIcon={IconTrash}
onClick={handleDelete}
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { lazy, type ReactElement, Suspense, useState } from 'react';
import { createPortal } from 'react-dom';
@@ -240,7 +241,7 @@ export const AttachmentList = ({
fallback={
<StyledLoadingContainer>
<StyledLoadingText>
Loading document viewer...
{t`Loading document viewer...`}
</StyledLoadingText>
</StyledLoadingContainer>
}
@@ -6,7 +6,7 @@ import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
import '@cyntler/react-doc-viewer/dist/index.css';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconDownload } from 'twenty-ui/display';
@@ -99,6 +99,7 @@ export const DocumentViewer = ({
documentName,
documentUrl,
}: DocumentViewerProps) => {
const { t } = useLingui();
const theme = useTheme();
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
@@ -141,7 +142,7 @@ export const DocumentViewer = ({
</StyledMessage>
<Button
Icon={IconDownload}
title="Download File"
title={t`Download File`}
onClick={() => downloadFile(documentUrl, documentName)}
variant="secondary"
/>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useDropzone } from 'react-dropzone';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -47,6 +48,7 @@ export const DropZone = ({
setIsDraggingFile,
onUploadFiles,
}: DropZoneProps) => {
const { t } = useLingui();
const theme = useTheme();
const { maxFileSize } = useSpreadsheetImportInternal();
@@ -85,9 +87,9 @@ export const DropZone = ({
stroke={theme.icon.stroke.sm}
size={theme.icon.size.lg}
/>
<StyledUploadDragTitle>Upload files</StyledUploadDragTitle>
<StyledUploadDragTitle>{t`Upload files`}</StyledUploadDragTitle>
<StyledUploadDragSubTitle>
Drag and Drop Here
{t`Drag and Drop Here`}
</StyledUploadDragSubTitle>
</>
)}
@@ -1,4 +1,5 @@
import { useContext } from 'react';
import { t } from '@lingui/core/macro';
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
@@ -95,7 +96,7 @@ export const ActivityTargetsInlineCell = ({
}}
/>
),
label: 'Relations',
label: t`Relations`,
displayModeContent: (
<ActivityTargetChips
activityTargetObjectRecords={activityTargetObjectRecords}
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { useActivityTargetsComponentInstanceId } from '@/activities/inline-cell/hooks/useActivityTargetsComponentInstanceId';
@@ -88,7 +89,7 @@ export const NoteTile = ({
})
}
>
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
<StyledNoteTitle>{note.title ?? t`Task Title`}</StyledNoteTitle>
<StyledCardContent>{body}</StyledCardContent>
</StyledCardDetailsContainer>
<StyledFooter>
@@ -68,10 +68,10 @@ export const NotesCard = () => {
<AnimatedPlaceholder type="noNote" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No notes
{t`No notes`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
There are no associated notes with this record.
{t`There are no associated notes with this record.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{hasObjectUpdatePermissions && (
@@ -78,10 +78,10 @@ export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
<AnimatedPlaceholder type="noTask" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
Mission accomplished!
{t`Mission accomplished!`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
All tasks addressed. Maintain the momentum.
{t`All tasks addressed. Maintain the momentum.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
{hasObjectUpdatePermissions && (
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
@@ -114,7 +115,7 @@ export const TaskRow = ({ task }: { task: Task }) => {
/>
</StyledCheckboxContainer>
<StyledTaskTitle completed={task.status === 'DONE'}>
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
{task.title || <StyledPlaceholder>{t`Task title`}</StyledPlaceholder>}
</StyledTaskTitle>
<StyledTaskBody>
<OverflowingTextWithTooltip text={body} />
@@ -72,10 +72,10 @@ export const TimelineCard = () => {
<AnimatedPlaceholder type="emptyTimeline" />
<AnimatedPlaceholderEmptyTextContainer>
<AnimatedPlaceholderEmptyTitle>
No activity yet
{t`No activity yet`}
</AnimatedPlaceholderEmptyTitle>
<AnimatedPlaceholderEmptySubTitle>
There is no activity associated with this record.
{t`There is no activity associated with this record.`}
</AnimatedPlaceholderEmptySubTitle>
</AnimatedPlaceholderEmptyTextContainer>
</EmptyContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import {
type EventRowDynamicComponentProps,
@@ -91,7 +92,7 @@ export const EventRowActivity = ({
return event.linkedRecordCachedName;
}
return 'Untitled';
return t`Untitled`;
};
const activityTitle = computeActivityTitle();
@@ -103,7 +104,7 @@ export const EventRowActivity = ({
<StyledRow>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
{`${eventAction} a related ${eventObject}`}
{t`${eventAction} a related ${eventObject}`}
</StyledEventRowItemAction>
<StyledLinkedActivity
onClick={() =>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { isUndefined } from '@sniptt/guards';
import { CalendarEventNotSharedContent } from '@/activities/calendar/components/CalendarEventNotSharedContent';
@@ -138,14 +139,14 @@ export const EventCardCalendarEvent = ({
);
if (shouldHandleNotFound) {
return <div>Calendar event not found</div>;
return <div>{t`Calendar event not found`}</div>;
}
return <div>Error loading calendar event</div>;
return <div>{t`Error loading calendar event`}</div>;
}
if (loading || isUndefined(calendarEvent)) {
return <div>Loading...</div>;
return <div>{t`Loading...`}</div>;
}
const startsAtDate = calendarEvent?.startsAt;
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent';
@@ -30,6 +31,7 @@ export const EventRowCalendarEvent = ({
authorFullName,
labelIdentifierValue,
}: EventRowCalendarEventProps) => {
const { t } = useLingui();
const [, eventAction] = event.name.split('.');
const [isOpen, setIsOpen] = useState(false);
@@ -42,7 +44,7 @@ export const EventRowCalendarEvent = ({
<StyledRowContainer>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
linked a calendar event with {labelIdentifierValue}
{t`linked a calendar event with ${labelIdentifierValue}`}
</StyledEventRowItemAction>
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
</StyledRowContainer>
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard';
@@ -54,6 +55,7 @@ export const EventRowMainObjectUpdated = ({
mainObjectMetadataItem,
createdAt,
}: EventRowMainObjectUpdatedProps) => {
const { t } = useLingui();
const diff: Record<string, { before: any; after: any }> =
event.properties?.diff;
@@ -70,12 +72,15 @@ export const EventRowMainObjectUpdated = ({
throw new Error('Cannot render update description without changes');
}
const fieldCount = diffEntries.length;
const recordLabel = labelIdentifierValue;
return (
<StyledEventRowMainObjectUpdatedContainer>
<StyledRowContainer>
<StyledRow>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
updated
{t`updated`}
{diffEntries.length === 1 && (
<EventFieldDiffContainer
mainObjectMetadataItem={mainObjectMetadataItem}
@@ -87,9 +92,7 @@ export const EventRowMainObjectUpdated = ({
)}
{diffEntries.length > 1 && (
<>
<span>
{diffEntries.length} fields on {labelIdentifierValue}
</span>
<span>{t`${fieldCount} fields on ${recordLabel}`}</span>
<EventCardToggleButton isOpen={isOpen} setIsOpen={setIsOpen} />
</>
)}
@@ -7,7 +7,7 @@ import { useOpenEmailThreadInCommandMenu } from '@/command-menu/hooks/useOpenEma
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
@@ -59,6 +59,7 @@ export const EventCardMessage = ({
messageId: string;
authorFullName: string;
}) => {
const { t } = useLingui();
const { upsertRecordsInStore } = useUpsertRecordsInStore();
const { openEmailThreadInCommandMenu } = useOpenEmailThreadInCommandMenu();
@@ -142,7 +143,7 @@ export const EventCardMessage = ({
{message.subject !==
FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED
? message.subject
: `Subject not shared`}
: t`Subject not shared`}
</StyledEmailTitle>
<StyledEmailParticipants>
<OverflowingTextWithTooltip text={messageParticipantHandles} />
@@ -1,4 +1,5 @@
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard';
@@ -42,7 +43,7 @@ export const EventRowMessage = ({
<StyledRowContainer>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
<StyledEventRowItemAction>
linked an email with
{t`linked an email with`}
</StyledEventRowItemAction>
<StyledEventRowItemColumn>
{labelIdentifierValue}
@@ -5,6 +5,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useToggleDropdown } from '@/ui/layout/dropdown/hooks/useToggleDropdown';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { type Editor } from '@tiptap/react';
import { useId } from 'react';
import { IconPilcrow } from 'twenty-ui/display';
@@ -50,7 +51,7 @@ export const TurnIntoBlockDropdown = ({
const options = useTurnIntoBlockOptions(editor);
const activeItem = options.find((option) => option.isActive());
const { icon: ActiveIcon = IconPilcrow, title: activeTitle = 'Paragraph' } =
const { icon: ActiveIcon = IconPilcrow, title: activeTitle = t`Paragraph` } =
activeItem ?? {};
return (
@@ -1,3 +1,4 @@
import { useLingui } from '@lingui/react/macro';
import { type Editor, useEditorState } from '@tiptap/react';
import {
type IconComponent,
@@ -17,12 +18,14 @@ export type TurnIntoBlockOptions = {
};
export const useTurnIntoBlockOptions = (editor: Editor) => {
const { t } = useLingui();
return useEditorState({
editor,
selector: ({ editor }): TurnIntoBlockOptions[] => [
{
id: 'paragraph',
title: 'Paragraph',
title: t`Paragraph`,
icon: IconPilcrow,
onClick: () => {
return editor.chain().focus().setParagraph().run();
@@ -36,7 +39,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading1',
title: 'Heading 1',
title: t`Heading 1`,
icon: IconH1,
onClick: () => {
return editor.chain().focus().setHeading({ level: 1 }).run();
@@ -50,7 +53,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading2',
title: 'Heading 2',
title: t`Heading 2`,
icon: IconH2,
onClick: () => {
return editor.chain().focus().setHeading({ level: 2 }).run();
@@ -64,7 +67,7 @@ export const useTurnIntoBlockOptions = (editor: Editor) => {
},
{
id: 'heading3',
title: 'Heading 3',
title: t`Heading 3`,
icon: IconH3,
onClick: () => {
return editor.chain().focus().setHeading({ level: 3 }).run();
@@ -51,16 +51,18 @@ export const useUploadWorkflowFile = () => {
createdAt: uploadedFile.createdAt,
};
const fileName = file.name;
enqueueSuccessSnackBar({
message: `File "${file.name}" uploaded successfully`,
message: t`File "${fileName}" uploaded successfully`,
});
return workflowFile;
} catch (error) {
logError(`Failed to upload workflow file "${file.name}": ${error}`);
const fileNameForError = file.name;
enqueueErrorSnackBar({
message: `Failed to upload "${file.name}"`,
message: t`Failed to upload "${fileNameForError}"`,
});
return null;
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { AvatarChip, ChipVariant, LinkChip } from 'twenty-ui/components';
@@ -29,6 +30,7 @@ export const RecordLink = ({
return (
<LinkChip
label={displayName}
emptyLabel={t`Untitled`}
to={linkToShowPage}
variant={ChipVariant.Highlighted}
leftComponent={
@@ -152,45 +152,51 @@ type TimingTabProps = {
};
const TimingTab = ({ debug }: TimingTabProps) => {
const { t } = useLingui();
const totalTime =
debug.agentExecutionStartTimeMs !== undefined
? `${debug.agentExecutionStartTimeMs + (debug.agentExecutionTimeMs || 0)}ms`
: undefined;
const totalCost =
debug.totalCostInCredits !== undefined
? formatNumber(debug.totalCostInCredits)
: undefined;
return (
<StyledTimingSection>
<TimingRow
label="Routing decision"
label={t`Routing decision`}
value={debug.routingTimeMs && `${debug.routingTimeMs}ms`}
/>
<TimingRow
label="Context building (routing)"
label={t`Context building (routing)`}
value={debug.contextBuildTimeMs && `${debug.contextBuildTimeMs}ms`}
/>
<TimingRow
label="Context building (agent)"
label={t`Context building (agent)`}
value={
debug.agentContextBuildTimeMs && `${debug.agentContextBuildTimeMs}ms`
}
/>
<TimingRow
label="Tool generation"
label={t`Tool generation`}
value={debug.toolGenerationTimeMs && `${debug.toolGenerationTimeMs}ms`}
/>
<TimingRow
label="AI request prep"
label={t`AI request prep`}
value={debug.aiRequestPrepTimeMs && `${debug.aiRequestPrepTimeMs}ms`}
/>
<TimingRow
label="Agent execution"
label={t`Agent execution`}
value={debug.agentExecutionTimeMs && `${debug.agentExecutionTimeMs}ms`}
/>
<TimingRow label="Total time" value={totalTime} />
<TimingRow label="Available tools" value={debug.toolCount} />
<TimingRow label="Tool calls made" value={debug.toolCallCount} />
<TimingRow label="Context records" value={debug.contextRecordCount} />
<TimingRow label={t`Total time`} value={totalTime} />
<TimingRow label={t`Available tools`} value={debug.toolCount} />
<TimingRow label={t`Tool calls made`} value={debug.toolCallCount} />
<TimingRow label={t`Context records`} value={debug.contextRecordCount} />
<TimingRow
label="Context size"
label={t`Context size`}
value={
debug.contextSizeBytes !== undefined
? formatBytes(debug.contextSizeBytes)
@@ -198,7 +204,7 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Routing tokens"
label={t`Routing tokens`}
value={
debug.routingTotalTokens !== undefined
? formatTokenBreakdown(
@@ -210,7 +216,7 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Agent tokens"
label={t`Agent tokens`}
value={
debug.agentTotalTokens !== undefined
? formatTokenBreakdown(
@@ -222,12 +228,8 @@ const TimingTab = ({ debug }: TimingTabProps) => {
}
/>
<TimingRow
label="Total cost"
value={
debug.totalCostInCredits !== undefined
? `${formatNumber(debug.totalCostInCredits)} credits`
: undefined
}
label={t`Total cost`}
value={totalCost !== undefined ? t`${totalCost} credits` : undefined}
/>
</StyledTimingSection>
);
@@ -279,7 +281,7 @@ const ContextTab = ({ debug, copyToClipboard }: ContextTabProps) => {
if (!debug.context) {
return (
<StyledTimingLabel>
No context was provided for this request
{t`No context was provided for this request`}
</StyledTimingLabel>
);
}
@@ -302,9 +304,10 @@ const ContextTab = ({ debug, copyToClipboard }: ContextTabProps) => {
</StyledJsonTreeContainer>
);
} catch {
const contextValue = debug.context;
return (
<StyledTimingLabel>
Failed to parse context: {debug.context}
{t`Failed to parse context: ${contextValue}`}
</StyledTimingLabel>
);
}
@@ -315,6 +318,7 @@ type RoutingDebugDisplayProps = {
};
export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
const { t } = useLingui();
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const [isExpanded, setIsExpanded] = useState(false);
@@ -323,7 +327,7 @@ export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
return (
<StyledContainer>
<StyledToggleButton onClick={() => setIsExpanded(!isExpanded)}>
<StyledTimingLabel>Debug Info</StyledTimingLabel>
<StyledTimingLabel>{t`Debug Info`}</StyledTimingLabel>
{isExpanded ? (
<IconChevronUp size={theme.icon.size.sm} />
) : (
@@ -338,20 +342,20 @@ export const RoutingDebugDisplay = ({ debug }: RoutingDebugDisplayProps) => {
isActive={activeTab === 'timing'}
onClick={() => setActiveTab('timing')}
>
Timing
{t`Timing`}
</StyledTab>
<StyledTab
isActive={activeTab === 'details'}
onClick={() => setActiveTab('details')}
>
Details
{t`Details`}
</StyledTab>
{debug.context && (
<StyledTab
isActive={activeTab === 'context'}
onClick={() => setActiveTab('context')}
>
Context
{t`Context`}
</StyledTab>
)}
</StyledTabContainer>
@@ -190,7 +190,7 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
}
const displayMessage = hasError
? 'Tool execution failed'
? t`Tool execution failed`
: output &&
typeof output === 'object' &&
'message' in output &&
@@ -240,13 +240,13 @@ export const ToolStepRenderer = ({ toolPart }: { toolPart: ToolUIPart }) => {
isActive={activeTab === 'output'}
onClick={() => setActiveTab('output')}
>
Output
{t`Output`}
</StyledTab>
<StyledTab
isActive={activeTab === 'input'}
onClick={() => setActiveTab('input')}
>
Input
{t`Input`}
</StyledTab>
</StyledTabContainer>
@@ -1,5 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { ProgressBar } from 'twenty-ui/feedback';
@@ -103,6 +104,7 @@ const formatTokenCount = (count: number): string => {
};
export const AIChatContextUsageButton = () => {
const { t } = useLingui();
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
const agentChatUsage = useRecoilValue(agentChatUsageState);
@@ -125,6 +127,8 @@ export const AIChatContextUsageButton = () => {
const formattedPercentage = percentage.toFixed(1);
const totalCredits =
agentChatUsage.inputCredits + agentChatUsage.outputCredits;
const inputCredits = agentChatUsage.inputCredits.toLocaleString();
const outputCredits = agentChatUsage.outputCredits.toLocaleString();
return (
<StyledContainer
@@ -162,23 +166,23 @@ export const AIChatContextUsageButton = () => {
<StyledBody>
<StyledRow>
<StyledLabel>Input</StyledLabel>
<StyledLabel>{t`Input`}</StyledLabel>
<StyledValue>
{formatTokenCount(agentChatUsage.inputTokens)} {' '}
{agentChatUsage.inputCredits.toLocaleString()} credits
{t`${inputCredits} credits`}
</StyledValue>
</StyledRow>
<StyledRow>
<StyledLabel>Output</StyledLabel>
<StyledLabel>{t`Output`}</StyledLabel>
<StyledValue>
{formatTokenCount(agentChatUsage.outputTokens)} {' '}
{agentChatUsage.outputCredits.toLocaleString()} credits
{t`${outputCredits} credits`}
</StyledValue>
</StyledRow>
</StyledBody>
<StyledFooter>
<StyledLabel>Total credits</StyledLabel>
<StyledLabel>{t`Total credits`}</StyledLabel>
<StyledPercentage>{totalCredits.toLocaleString()}</StyledPercentage>
</StyledFooter>
</StyledHoverCard>
@@ -3,6 +3,7 @@ import { getFileType } from '@/activities/files/utils/getFileType';
import { useFileCategoryColors } from '@/file/hooks/useFileCategoryColors';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { useTheme } from '@emotion/react';
import { t } from '@lingui/core/macro';
import { type FileUIPart } from 'ai';
import { isDefined } from 'twenty-shared/utils';
import { AvatarChip, Chip, ChipVariant, LinkChip } from 'twenty-ui/components';
@@ -23,7 +24,7 @@ export const AgentChatFilePreview = ({
useFileCategoryColors();
const fileName =
file instanceof File ? file.name : (file.filename ?? 'Unknown file');
file instanceof File ? file.name : (file.filename ?? t`Unknown file`);
const fileUrl = file instanceof File ? undefined : file.url;
@@ -54,6 +55,7 @@ export const AgentChatFilePreview = ({
return (
<LinkChip
label={fileName}
emptyLabel={t`Untitled`}
variant={ChipVariant.Static}
to={fileUrl}
target="_blank"
@@ -66,6 +68,7 @@ export const AgentChatFilePreview = ({
return (
<Chip
label={fileName}
emptyLabel={t`Untitled`}
variant={ChipVariant.Static}
clickable={false}
leftComponent={leftComponent}
@@ -8,6 +8,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { t } from '@lingui/core/macro';
import { useRecoilCallback } from 'recoil';
export const useGetBrowsingContext = () => {
@@ -103,7 +104,7 @@ export const useGetBrowsingContext = () => {
const fieldMetadataItem = objectMetadataItem.fields.find(
(field) => field.id === filter.fieldMetadataId,
);
const fieldLabel = fieldMetadataItem?.label ?? 'Unknown field';
const fieldLabel = fieldMetadataItem?.label ?? t`Unknown field`;
return `${fieldLabel} ${filter.operand} "${filter.displayValue}"`;
});
@@ -1,6 +1,7 @@
import { type Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { Controller, useFormContext } from 'react-hook-form';
import { isDefined } from 'twenty-shared/utils';
@@ -20,6 +21,7 @@ export const SignInUpEmailField = ({
showErrors: boolean;
onInputChange?: (value: string) => void;
}) => {
const { t } = useLingui();
const form = useFormContext<Form>();
return (
@@ -45,7 +47,7 @@ export const SignInUpEmailField = ({
autoComplete="email"
autoFocus
value={value}
placeholder="Email"
placeholder={t`Email`}
onBlur={onBlur}
onChange={(email: string) => {
if (isDefined(onInputChange)) onInputChange(email);
@@ -50,7 +50,7 @@ export const SignInUpPasswordField = ({
autoFocus
value={value}
type="password"
placeholder="Password"
placeholder={t`Password`}
onBlur={onBlur}
onChange={onChange}
error={showErrors ? error?.message : undefined}
@@ -1,4 +1,5 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from '@lingui/core/macro';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useSearchParams } from 'react-router-dom';
@@ -20,12 +21,12 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
email: z
.string()
.trim()
.pipe(z.email({ error: 'Email must be a valid email' })),
.pipe(z.email({ error: t`Email must be a valid email` })),
password:
signInUpStep === SignInUpStep.Password
? z
.string()
.regex(PASSWORD_REGEX, 'Password must be min. 8 characters')
.regex(PASSWORD_REGEX, t`Password must be min. 8 characters`)
: z.string().optional(),
captchaToken: z.string().default(''),
})

Some files were not shown because too many files have changed in this diff Show More