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
+489 -4
View File
@@ -128,6 +128,375 @@ export default [
additionalHooks: 'useRecoilCallback',
},
],
// Lingui - detect untranslated strings
'lingui/no-unlocalized-strings': [
'error',
{
ignore: [
// Ignore strings which are a single "word" (no spaces) and don't start with uppercase
'^(?![A-Z])\\S+$',
// Ignore UPPERCASE literals (constants, env vars)
'^[A-Z0-9_-]+$',
// Ignore strings that look like code/technical (contain special chars)
'^[\\s]*$', // whitespace only
'.*[{}/<>].*', // contains code-like characters
'^\\d+(\\.\\d+)?(px|rem|em|%|vh|vw|s|ms)?$', // CSS units
'^[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?(\\s+[\\d.]+(px|rem|em|%|vh|vw|fr|s|ms)?)*$', // CSS values like "200px 1fr 20px"
'^#[0-9a-fA-F]{3,8}$', // hex colors
'^rgba?\\(.*\\)$', // rgb/rgba colors
'^(auto|none|inherit|initial|unset|flex|grid|block|inline|inline-block|relative|absolute|fixed|sticky)$', // CSS keywords
'^color:.*$', // CSS color declarations
'^font-.*$', // CSS font declarations
'^\\d+$', // numbers only
'^https?:\\/\\/.*', // URLs
'^@.*', // @ mentions or decorators
'^\\/.*', // paths starting with /
'^[HhMmSsYyDdAaPp:.,\\s-]+$', // date format patterns (HH:mm, yyyy-MM-dd, etc.)
'^Arrow(Up|Down|Left|Right)$', // keyboard keys
'^(Enter|Escape|Tab|Space|Backspace|Delete)$', // keyboard keys
'^Text$', // clipboard data type
'^(allow-|sandbox)', // iframe sandbox values
'^Id$', // technical identifier suffix (e.g., fieldNameId)
'^(string|number|boolean|void|any|unknown|never|object)$', // TypeScript type keywords
'^(Dark|Light)$', // color schemes
'^translate\\(.*\\)$', // CSS transform strings
'^svg .*$', // CSS selectors
'^Icon[A-Z]\\w*$', // Icon names like IconDefault, IconTable, IconSettings
'^\\w*Icon$', // Icon names that end with Icon like FieldIcon
'^%c.*$', // Console format strings
// Common item IDs for selectable lists
'^(Group|CalendarView|CalendarDateField|Compact view)$',
'^(Layout|Visibility|Fields|Delete view|Copy link to view|Create custom view)$',
'^(GroupBy|Sort|HideEmptyGroups|HiddenGroups)$',
// HTTP headers and auth (technical, not user-facing)
'^Authorization$',
'^Bearer .*',
// Allow object keys that are technical identifiers
'^(topLeft|topRight|bottomLeft|bottomRight)$',
// Color schemes and CSS media queries
'^System$',
'^\\(prefers-color-scheme:',
// GraphQL query names (used in refetchQueries)
'^Get[A-Z]\\w*$',
// React Context names (technical identifiers)
'.*Context$',
// SVG paths (geometric coordinates, not translatable)
'^M[0-9 LML]+$',
'^[ML][0-9 ]+$',
// Database ordering values (technical, backend API)
'^(Asc|Desc)Nulls(First|Last)$',
// Calendar response status values (backend enum values, not user-facing)
'^(Yes|No|Maybe)$',
// Email validation error prefixes (combined with dynamic content)
'^Invalid email(s)?:',
// GraphQL type construction patterns
'.*FilterInput$',
'.*OrderByInput.*',
'^\\$filter.*',
'^\\$orderBy.*',
'^\\$after.*',
'^\\$before.*',
'^\\$first.*',
'^\\$last.*',
// Logger names (technical identifiers)
'^Twenty(-\\w+)?$',
// Cookie names and cookie string patterns
'^twenty_session_id$',
'^; domain=',
// Context names for createRequiredContext
'^[A-Z][a-zA-Z]+$',
// JSON-like filter patterns
'^%"type":',
'^%"objectNameSingular":',
],
ignoreNames: [
// HTML/React attributes that shouldn't be translated
{ regex: { pattern: 'className', flags: 'i' } },
{ regex: { pattern: 'styleName', flags: 'i' } },
{ regex: { pattern: 'testId', flags: 'i' } },
'data-testid',
'dataTestId',
'src',
'srcSet',
'href',
'target',
'rel',
'type',
'id',
'key',
'name',
'htmlFor',
'width',
'height',
'fill',
'stroke',
'viewBox',
'clipPath',
'd', // SVG path
'transform',
'displayName',
'defaultValue',
'to', // router links
'path',
'pathname',
'hash',
'componentInstanceId',
'hotkeyScope',
'dropdownId',
'recoilScopeId',
'modalId',
'dialogId',
'itemId',
'selectableItemIdArray',
'listenerId',
'focusId',
'color', // color prop values
'variant', // component variants
'size', // size prop values
'position', // position values
'align', // alignment values
'justify', // justification values
'direction', // direction values
'orientation', // orientation values
'status', // status values
'state', // state values
'mode', // mode values
'accent', // accent values
// CSS-related props
'gridAutoColumns',
'gridAutoRows',
'gridTemplateColumns',
'gridTemplateRows',
'gridColumn',
'gridRow',
'gap',
'margin',
'padding',
'border',
'borderRadius',
'boxShadow',
'flex',
'flexDirection',
'flexWrap',
'justifyContent',
'alignItems',
'alignContent',
'overflow',
'display',
'cursor',
'zIndex',
'opacity',
'fontWeight',
'fontSize',
'lineHeight',
'textAlign',
'textDecoration',
'whiteSpace',
'wordBreak',
'objectFit',
'backgroundSize',
'backgroundPosition',
'minWidth',
'maxWidth',
'minHeight',
'maxHeight',
'mobileGridAutoColumns',
'tabletGridAutoColumns',
// Styled components
'css',
'theme',
'animation',
'transition',
// GraphQL
'query',
'mutation',
'subscription',
'fragment',
'operationName',
'variables',
'__typename',
// Technical identifiers
'fieldName',
'columnName',
'objectNameSingular',
'objectNamePlural',
'metadataId',
'nameSingular',
'namePlural',
// Event types
'eventName',
'event',
'action',
'actionType',
// Icon names
'iconName',
{ regex: { pattern: '^Icon[A-Z]' } },
// UPPER_CASE names (constants)
{ regex: { pattern: '^[A-Z][A-Z0-9_]*$' } },
// Sort direction values (backend API)
'orderBy',
{ regex: { pattern: '^(Asc|Desc)(NullsFirst|NullsLast)?$' } },
// HTTP headers (technical, not user-facing)
'Authorization',
],
ignoreFunctions: [
// Console and logging
'console.*',
'*.log',
'*.warn',
'*.error',
'*.debug',
'*.info',
'*.trace',
'logDebug',
'formatTitle',
// Error handling (technical messages, not user-facing)
'Error',
'TypeError',
'RangeError',
'SyntaxError',
'throw',
'assertUnreachable',
'CustomError',
'parseInitialBlocknote',
// Testing
'describe',
'it',
'test',
'expect',
'jest.*',
'*.toBe',
'*.toEqual',
'*.toContain',
'*.toMatch',
'*.toThrow',
// React/Libraries internals
'require',
'import',
'styled',
'styled.*',
'css',
'keyframes',
'createGlobalStyle',
// Router
'useNavigate',
'navigate',
'useLocation',
'useParams',
// Date formatting (patterns are not translatable)
'format',
'formatDate',
'formatDateTime',
'formatTime',
'parseISO',
'parse',
// Navigation
'useNavigationSection',
// Recoil
'atom',
'atomFamily',
'selector',
'selectorFamily',
'useSetRecoilState',
'useRecoilState',
'useRecoilValue',
// GraphQL operations
'gql',
'useQuery',
'useMutation',
'useLazyQuery',
'useSubscription',
// Type checking and validation
'*.includes',
'*.indexOf',
'*.startsWith',
'*.endsWith',
'*.split',
'*.join',
'*.match',
'*.replace',
'*.test',
'Object.keys',
'Object.values',
'Object.entries',
'Array.isArray',
// DOM operations
'*.getElementById',
'*.getElementsByClassName',
'*.querySelector',
'*.querySelectorAll',
'*.getAttribute',
'*.setAttribute',
'*.addEventListener',
'*.removeEventListener',
'*.dispatchEvent',
'*.createElement',
// Storage
'localStorage.*',
'sessionStorage.*',
'searchParams.*',
'*.get',
'*.set',
'*.has',
'*.delete',
// Misc utilities
'cva',
'cn',
'clsx',
'classNames',
'track',
'*.postMessage',
'*.dispatch',
'*.commit',
// Event handlers (typically receive enum values, not user-facing text)
'onChange',
'onClick',
'onSelect',
'onSubmit',
'onFocus',
'onBlur',
'onKeyDown',
'onKeyUp',
'onMouseEnter',
'onMouseLeave',
// Logging functions (technical messages, not user-facing)
'logError',
'logDebug',
'logInfo',
'logWarn',
'loggerLink',
// Context creation (technical names)
'createRequiredContext',
// GraphQL refetch queries (technical identifiers)
'refetchQueries',
],
},
],
},
},
@@ -175,7 +544,7 @@ export default [
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{
{
prefer: 'type-imports',
fixStyle: 'inline-type-imports'
},
@@ -208,11 +577,115 @@ export default [
},
},
// Storybook files
// Storybook files and story-related files
{
files: ['*.stories.@(ts|tsx|js|jsx)'],
files: [
'**/*.stories.ts',
'**/*.stories.tsx',
'**/*.stories.js',
'**/*.stories.jsx',
'**/__stories__/**/*',
],
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},
// Debug files - development only, not user-facing
{
files: [
'**/Debug*.tsx',
'**/*Debug*.tsx',
'**/*DebugDisplay*.tsx',
'**/*DebugHelper*.tsx',
'**/*DebugObserver*.tsx',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Testing utilities and mock data - not user-facing
{
files: [
'**/testing/**/*.tsx',
'**/testing/**/*.ts',
'**/__mocks__/**/*',
'**/*mock*.ts',
'**/*Mock*.ts',
'**/perf/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Constants files - technical values, not user-facing
{
files: [
'**/constants/**/*.ts',
'**/*.constants.ts',
'**/validation-schemas/**/*.ts',
'**/*Schema.ts',
'**/*-schema.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Layout configuration files - titles are translated at consumption time
{
files: ['**/layouts/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Service files - contain technical strings (logger names, HTTP headers, etc.)
{
files: ['**/services/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// State files - contain technical default values
{
files: ['**/states/**/*.ts'],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Utility files - technical/developer-facing
{
files: [
'**/utils/**/*.ts',
'**/*Utils.ts',
'**/*-utils.ts',
'**/*Util.ts',
'**/*-util.ts',
'**/errors/**/*.ts',
'**/*Error.ts',
'**/*-error.ts',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
// Config and setup files - not user-facing
{
files: [
'**/*.config.ts',
'**/*.config.js',
'**/vite.config.ts',
'**/.storybook/**/*',
],
rules: {
'lingui/no-unlocalized-strings': 'off',
},
},
@@ -250,7 +723,18 @@ export default [
// Test files
{
files: [
'*.test.@(ts|tsx|js|jsx)',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.test.js',
'**/*.test.jsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
'**/__tests__/**/*.ts',
'**/__tests__/**/*.tsx',
'**/__mocks__/**/*.ts',
'**/__mocks__/**/*.tsx',
],
languageOptions: {
globals: {
@@ -266,6 +750,7 @@ export default [
},
rules: {
'@typescript-eslint/no-non-null-assertion': 'off',
'lingui/no-unlocalized-strings': 'off',
},
},