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
@@ -7,6 +7,8 @@ import { z } from 'zod';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList';
import { i18n } from '@lingui/core';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { isDefined } from 'twenty-shared/utils';
import { IconSend } from 'twenty-ui/display';
@@ -24,42 +26,41 @@ const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
`;
const emailValidationSchema = (email: string) =>
z.email(`Invalid email '${email}'`);
const emailsEmptyErrorMessage = msg`Emails should not be empty`;
const validationSchema = () =>
z
.object({
emails: z.string().superRefine((value, ctx) => {
if (!value.length) {
return;
const validationSchema = z
.object({
emails: z.string().superRefine((value, ctx) => {
if (!value.length) {
return;
}
const emails = sanitizeEmailList(value.split(','));
if (emails.length === 0) {
ctx.addIssue({
code: 'custom',
message: i18n._(emailsEmptyErrorMessage),
});
}
const invalidEmails: string[] = [];
for (const email of emails) {
const result = z.email().safeParse(email);
if (!result.success) {
invalidEmails.push(email);
}
const emails = sanitizeEmailList(value.split(','));
if (emails.length === 0) {
ctx.addIssue({
code: 'custom',
message: 'Emails should not be empty',
});
}
const invalidEmails: string[] = [];
for (const email of emails) {
const result = emailValidationSchema(email).safeParse(email);
if (!result.success) {
invalidEmails.push(email);
}
}
if (invalidEmails.length > 0) {
ctx.addIssue({
code: 'custom',
message:
invalidEmails.length > 1
? 'Emails "' + invalidEmails.join('", "') + '" are invalid'
: 'Email "' + invalidEmails.join('", "') + '" is invalid',
});
}
}),
})
.required();
}
if (invalidEmails.length > 0) {
const invalidEmailsList = invalidEmails.join(', ');
ctx.addIssue({
code: 'custom',
message:
invalidEmails.length > 1
? `Invalid emails: ${invalidEmailsList}`
: `Invalid email: ${invalidEmailsList}`,
});
}
}),
})
.required();
type FormInput = {
emails: string;
@@ -74,7 +75,7 @@ export const WorkspaceInviteTeam = () => {
const { reset, handleSubmit, control, formState, watch } = useForm<FormInput>(
{
mode: 'onSubmit',
resolver: zodResolver(validationSchema()),
resolver: zodResolver(validationSchema),
defaultValues: {
emails: '',
},
@@ -86,8 +87,9 @@ export const WorkspaceInviteTeam = () => {
const emailsList = sanitizeEmailList(emails.split(','));
const { data } = await sendInvitation({ emails: emailsList });
if (isDefined(data) && data.sendInvitations.result.length > 0) {
const invitationCount = data.sendInvitations.result.length;
enqueueSuccessSnackBar({
message: `${data.sendInvitations.result.length} invitations sent`,
message: t`${invitationCount} invitations sent`,
options: {
duration: 2000,
},
@@ -122,6 +124,7 @@ export const WorkspaceInviteTeam = () => {
return (
<SettingsTextInput
instanceId="workspace-invite-team-emails"
// eslint-disable-next-line lingui/no-unlocalized-strings
placeholder="tim@apple.com, jony.ive@apple.dev"
value={value}
onChange={onChange}