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
@@ -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],
);