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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user