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
@@ -95,7 +95,7 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
return (
<StyledContainer>
New Field <StyledSpan>-</StyledSpan>
{t`New Field`} <StyledSpan>-</StyledSpan>
<Dropdown
dropdownPlacement="bottom-start"
dropdownId={dropdownId}
@@ -9,6 +9,7 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
import { t } from '@lingui/core/macro';
import { type MouseEvent, useMemo, useState } from 'react';
import { type IconComponent } from 'twenty-ui/display';
import { type SelectOption } from 'twenty-ui/input';
@@ -72,7 +73,7 @@ export const MultiSelectAddressFields = <Value extends SelectValue>({
selectedOption={{
label:
values?.length === options.length
? 'Default'
? t`Default`
: values?.length.toString(),
value: values?.length,
}}
@@ -49,7 +49,7 @@ export const SettingsDataModelFieldAddressForm = ({
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
const countries = [
{
label: 'No country',
label: t`No country`,
value: '',
Icon: IconCircleOff,
},
@@ -140,7 +140,7 @@ export const SettingsObjectNewFieldSelector = ({
[
key,
key === FieldMetadataType.MORPH_RELATION
? { ...config, label: 'Relation' }
? { ...config, label: t`Relation` }
: config,
] as [string, SettingsFieldTypeConfig<any>],
)
@@ -1,3 +1,4 @@
import { t } from '@lingui/core/macro';
import { Separator } from '@/settings/components/Separator';
import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard';
import { SettingsDataModelFieldIsUniqueForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIsUniqueForm';
@@ -25,7 +26,7 @@ export const SettingsDataModelFieldNumberSettingsFormCard = ({
<SettingsDataModelFieldPreviewWidget
fieldMetadataItem={{
icon: watch('icon'),
label: watch('label') || 'New Field',
label: watch('label') || t`New Field`,
settings: watch('settings') || null,
type: FieldMetadataType.NUMBER,
}}
@@ -18,10 +18,42 @@ import {
IconX,
} from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem, MenuItemSelectColor } from 'twenty-ui/navigation';
import {
type ColorLabels,
MenuItem,
MenuItemSelectColor,
} from 'twenty-ui/navigation';
import { MAIN_COLOR_NAMES } from 'twenty-ui/theme';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
const useColorLabels = (): ColorLabels => ({
gray: t`Gray`,
tomato: t`Tomato`,
red: t`Red`,
ruby: t`Ruby`,
crimson: t`Crimson`,
pink: t`Pink`,
plum: t`Plum`,
purple: t`Purple`,
violet: t`Violet`,
iris: t`Iris`,
cyan: t`Cyan`,
turquoise: t`Turquoise`,
sky: t`Sky`,
blue: t`Blue`,
jade: t`Jade`,
green: t`Green`,
grass: t`Grass`,
mint: t`Mint`,
lime: t`Lime`,
bronze: t`Bronze`,
gold: t`Gold`,
brown: t`Brown`,
orange: t`Orange`,
amber: t`Amber`,
yellow: t`Yellow`,
});
type SettingsDataModelFieldSelectFormOptionRowProps = {
className?: string;
isDefault?: boolean;
@@ -51,7 +83,7 @@ const StyledColorSample = styled(ColorSample)`
margin-left: ${({ theme }) => theme.spacing(3.5)};
`;
const StyledOptionInput = styled(SettingsTextInput)`
const StyledOptionInput = styled(SettingsTextInput)`Chip
flex-grow: 1;
width: 100%;
& input {
@@ -80,6 +112,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
fieldIsNullable,
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
const theme = useTheme();
const colorLabels = useColorLabels();
const SELECT_COLOR_DROPDOWN_ID = `select-color-dropdown-${option.id}`;
const SELECT_ACTIONS_DROPDOWN_ID = `select-actions-dropdown-${option.id}`;
@@ -130,6 +163,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
}}
color={colorName}
selected={colorName === option.color}
colorLabels={colorLabels}
/>
))}
</DropdownMenuItemsContainer>
@@ -1,3 +1,4 @@
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { v4 } from 'uuid';
@@ -7,12 +8,15 @@ import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetad
import { type SettingsDataModelFieldSelectFormValues } from '@/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm';
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/computeOptionValueFromLabel';
const DEFAULT_OPTION: FieldMetadataItemOption = {
color: 'green',
id: v4(),
label: 'Option 1',
position: 0,
value: computeOptionValueFromLabel('Option 1'),
const getDefaultOption = (): FieldMetadataItemOption => {
const label = t`Option 1`;
return {
color: 'green',
id: v4(),
label,
position: 0,
value: computeOptionValueFromLabel(label),
};
};
type UseSelectSettingsFormInitialValuesProps = {
@@ -33,7 +37,7 @@ export const useSelectSettingsFormInitialValues = ({
? [...fieldMetadataItem.options].sort(
(optionA, optionB) => optionA.position - optionB.position,
)
: [DEFAULT_OPTION],
: [getDefaultOption()],
[fieldMetadataItem?.options],
);
@@ -30,9 +30,9 @@ export const SettingsAvailableStandardObjectsSection = ({
<Table>
<StyledAvailableStandardObjectTableRow>
<TableHeader></TableHeader>
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader align="right">Fields</TableHeader>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Description`}</TableHeader>
<TableHeader align="right">{t`Fields`}</TableHeader>
</StyledAvailableStandardObjectTableRow>
<TableBody>
{objectItems.map((objectItem) => (
@@ -59,7 +59,7 @@ export const SettingsObjectFieldInactiveActionDropdown = ({
dropdownId={dropdownId}
clickableComponent={
<LightIconButton
aria-label="Inactive Field Options"
aria-label={t`Inactive Field Options`}
Icon={IconDotsVertical}
accent="tertiary"
/>
@@ -163,15 +163,15 @@ export const SettingsObjectFieldItemTableRow = ({
const typeLabel =
variant === 'field-type'
? isRemoteObjectField
? 'Remote'
? t`Remote`
: fieldMetadataItem.isCustom
? 'Custom'
: 'Standard'
? t`Custom`
: t`Standard`
: variant === 'identifier'
? isDefined(identifierType)
? identifierType === 'label'
? 'Record text'
: 'Record image'
? t`Record text`
: t`Record image`
: ''
: '';
@@ -181,9 +181,10 @@ export const SettingsObjectFieldItemTableRow = ({
isDefined(relationObjectMetadataItem?.namePlural) &&
!relationObjectMetadataItem.isSystem;
const morphRelationCount = fieldMetadataItem.morphRelations?.length;
const morphRelationLabel =
fieldMetadataItem.type === FieldMetadataType.MORPH_RELATION
? `${fieldMetadataItem.morphRelations?.length} Objects`
? t`${morphRelationCount} Objects`
: undefined;
const label = morphRelationLabel
@@ -1,3 +1,4 @@
import { t } from '@lingui/core/macro';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@@ -113,7 +114,7 @@ const SettingsDataModelObjectPreviewOtherObjects = ({
/>
</StyledIconContainer>
<StyledOverflowingTextWithTooltip>
<OverflowingTextWithTooltip text={`Other objects`} />
<OverflowingTextWithTooltip text={t`Other objects`} />
</StyledOverflowingTextWithTooltip>
</StyledObjectName>
<StyledNumber>
@@ -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 { t } from '@lingui/core/macro';
import { IconArchiveOff, IconDotsVertical, IconTrash } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
@@ -39,7 +40,7 @@ export const SettingsObjectInactiveMenuDropDown = ({
dropdownId={dropdownId}
clickableComponent={
<LightIconButton
aria-label="Inactive Object Options"
aria-label={t`Inactive Object Options`}
Icon={IconDotsVertical}
accent="tertiary"
/>
@@ -48,13 +49,13 @@ export const SettingsObjectInactiveMenuDropDown = ({
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
<DropdownMenuItemsContainer>
<MenuItem
text="Activate"
text={t`Activate`}
LeftIcon={IconArchiveOff}
onClick={handleActivate}
/>
{isCustomObject && (
<MenuItem
text="Delete"
text={t`Delete`}
LeftIcon={IconTrash}
accent="danger"
onClick={handleDelete}
@@ -2,6 +2,7 @@ import { type Decorator, type Meta, type StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SettingsObjectInactiveMenuDropDown } from '../SettingsObjectInactiveMenuDropDown';
const handleActivateMockFunction = fn();
@@ -23,7 +24,7 @@ const meta: Meta<typeof SettingsObjectInactiveMenuDropDown> = {
onActivate: handleActivateMockFunction,
onDelete: handleDeleteMockFunction,
},
decorators: [ComponentDecorator, ClearMocksDecorator],
decorators: [I18nFrontDecorator, ComponentDecorator, ClearMocksDecorator],
parameters: {
clearMocks: true,
},
@@ -93,7 +93,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
const emptyOption: SelectOption<string | null> = {
Icon: IconCircleOff,
label: 'None',
label: t`None`,
value: null,
};