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
@@ -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}"`;
});