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:
+1
-1
@@ -95,7 +95,7 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
New Field <StyledSpan>-</StyledSpan>
|
||||
{t`New Field`} <StyledSpan>-</StyledSpan>
|
||||
<Dropdown
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownId={dropdownId}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
}}
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ export const SettingsDataModelFieldAddressForm = ({
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
const countries = [
|
||||
{
|
||||
label: 'No country',
|
||||
label: t`No country`,
|
||||
value: '',
|
||||
Icon: IconCircleOff,
|
||||
},
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ export const SettingsObjectNewFieldSelector = ({
|
||||
[
|
||||
key,
|
||||
key === FieldMetadataType.MORPH_RELATION
|
||||
? { ...config, label: 'Relation' }
|
||||
? { ...config, label: t`Relation` }
|
||||
: config,
|
||||
] as [string, SettingsFieldTypeConfig<any>],
|
||||
)
|
||||
|
||||
+2
-1
@@ -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,
|
||||
}}
|
||||
|
||||
+36
-2
@@ -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>
|
||||
|
||||
+11
-7
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -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) => (
|
||||
|
||||
+1
-1
@@ -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"
|
||||
/>
|
||||
|
||||
+7
-6
@@ -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
|
||||
|
||||
+2
-1
@@ -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>
|
||||
|
||||
+4
-3
@@ -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
-1
@@ -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,
|
||||
},
|
||||
|
||||
+1
-1
@@ -93,7 +93,7 @@ export const SettingsDataModelObjectIdentifiersForm = ({
|
||||
|
||||
const emptyOption: SelectOption<string | null> = {
|
||||
Icon: IconCircleOff,
|
||||
label: 'None',
|
||||
label: t`None`,
|
||||
value: null,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user