Skip to content

Commit 4d4955a

Browse files
committed
feat(no-unlocalized-strings): auto-ignore UPPER_CASE styling constants
- Matches *_COLORS, *_CLASSES, *_STYLES, *_ICONS, *_IMAGES, *_SIZES, *_IDS - Useful for Tailwind CSS class mappings like STATUS_COLORS - Also adds Image suffix to camelCase props (backgroundImage, avatarImage)
1 parent acf82ce commit 4d4955a

File tree

4 files changed

+105
-3
lines changed

4 files changed

+105
-3
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ date.toLocaleDateString("de-DE", { weekday: "long" }) // Intl.DateTimeFormatOpt
2222
type Status = "idle" | "loading" | "error"
2323
const status: Status = "loading" // String literal union
2424

25-
// ✅ Automatically ignored - styling props and numeric strings
25+
// ✅ Automatically ignored - styling props, constants, and numeric strings
2626
<Box containerClassName="flex items-center" /> // *ClassName, *Class, *Color, etc.
27+
const STATUS_COLORS = { active: "bg-green-100" } // *_COLORS, *_CLASSES, etc.
2728
const price = "1,00€" // No letters = technical
2829

2930
// ❌ Reported - actual user-visible text
@@ -52,7 +53,8 @@ const label = t("save") // ❌ Not confused with Lingui
5253
- 📝 Enforces simple, safe expressions inside translated messages
5354
- 🎯 Detects missing localization of user-visible text
5455
- 🧠 Zero-config recognition of technical strings via TypeScript types
55-
- 🎨 Auto-ignores styling props (`*ClassName`, `*Color`, `*Style`, `*Icon`, `*Size`, `*Id`)
56+
- 🎨 Auto-ignores styling props (`*ClassName`, `*Color`, `*Style`, `*Icon`, `*Image`, `*Size`, `*Id`)
57+
- 📦 Auto-ignores styling constants (`STATUS_COLORS`, `BUTTON_CLASSES`, `THEME_STYLES`, etc.)
5658
- 🔢 Auto-ignores numeric/symbolic strings without letters (`"1,00€"`, `"12:30"`)
5759
- 🔒 Verifies Lingui macros actually come from `@lingui/*` packages (no false positives from similarly-named functions)
5860

@@ -133,6 +135,7 @@ This plugin is a TypeScript-focused alternative to the official [eslint-plugin-l
133135
| **DOM API strings** | Manual whitelist | ✅ Auto-detected |
134136
| **Intl method arguments** | Manual whitelist | ✅ Auto-detected |
135137
| **Styling props** (`*ClassName`, etc.) | Manual whitelist | ✅ Auto-detected |
138+
| **Styling constants** (`*_COLORS`, etc.) | Manual whitelist | ✅ Auto-detected |
136139
| **Numeric strings** (`"1,00€"`) | Manual whitelist | ✅ Auto-detected |
137140
| **Lingui macro verification** | Name-based only | ✅ Verifies package origin |
138141
| **ESLint version** | 8.x | 9.x (flat config) |

docs/rules/no-unlocalized-strings.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ In addition to the explicit list, the rule automatically ignores camelCase prope
164164
| `Color` | `backgroundColor`, `borderColor` |
165165
| `Style` | `containerStyle`, `buttonStyle` |
166166
| `Icon` | `leftIcon`, `statusIcon` |
167+
| `Image` | `backgroundImage`, `avatarImage` |
167168
| `Size` | `fontSize`, `iconSize` |
168169
| `Id` | `containerId`, `elementId` |
169170

@@ -172,11 +173,41 @@ In addition to the explicit list, the rule automatically ignores camelCase prope
172173
<Button containerClassName="flex items-center" />
173174
<Input wrapperClassName="mt-4" />
174175
<Box backgroundColor="#ff0000" />
176+
<Card backgroundImage="url(/hero.jpg)" />
175177
<Avatar iconSize="24" />
176178
```
177179

178180
This covers common patterns in component libraries like Chakra UI, Material UI, and custom component props.
179181

182+
#### Auto-Detected Styling Constants
183+
184+
UPPER_CASE constant names with styling-related suffixes are also automatically ignored:
185+
186+
| Suffix | Examples |
187+
|--------|----------|
188+
| `_CLASS`, `_CLASSES`, `_CLASSNAME`, `_CLASSNAMES` | `BUTTON_CLASSES`, `CARD_CLASSNAME` |
189+
| `_COLOR`, `_COLORS` | `STATUS_COLORS`, `THEME_COLOR` |
190+
| `_STYLE`, `_STYLES` | `CARD_STYLES`, `INPUT_STYLE` |
191+
| `_ICON`, `_ICONS` | `NAV_ICONS`, `STATUS_ICON` |
192+
| `_IMAGE`, `_IMAGES` | `HERO_IMAGES`, `CARD_IMAGE` |
193+
| `_SIZE`, `_SIZES` | `AVATAR_SIZES`, `FONT_SIZE` |
194+
| `_ID`, `_IDS` | `ELEMENT_IDS`, `SECTION_ID` |
195+
196+
```tsx
197+
// All string values inside these constants are automatically ignored
198+
const STATUS_COLORS = {
199+
active: "bg-green-100 text-green-800",
200+
error: "bg-red-100 text-red-800",
201+
}
202+
203+
const BUTTON_CLASSES = {
204+
primary: "px-4 py-2 bg-blue-500 text-white rounded",
205+
secondary: "px-4 py-2 bg-gray-200 text-gray-800 rounded",
206+
}
207+
```
208+
209+
This is useful for Tailwind CSS class mappings and similar styling configuration objects.
210+
180211
### `ignoreNames`
181212

182213
Array of variable names to ignore.

src/rules/no-unlocalized-strings.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,21 @@ ruleTester.run("no-unlocalized-strings", noUnlocalizedStrings, {
112112
// Id properties
113113
{ code: '<Section containerId="main-section" />', filename: "test.tsx" },
114114
{ code: '({ elementId: "header" })', filename: "test.tsx" },
115+
// Image properties
116+
{ code: '<Card backgroundImage="url(/bg.png)" />', filename: "test.tsx" },
117+
{ code: '({ avatarImage: "user-default.svg" })', filename: "test.tsx" },
118+
119+
// UPPER_CASE styling constants
120+
{ code: 'const STATUS_COLORS = { active: "bg-green-100 text-green-800" }', filename: "test.tsx" },
121+
{ code: 'const BUTTON_CLASSES = { primary: "px-4 py-2 rounded" }', filename: "test.tsx" },
122+
{ code: 'const THEME_STYLES = { dark: "bg-gray-900 text-white" }', filename: "test.tsx" },
123+
{ code: 'const NAV_ICONS = { home: "house-solid", settings: "gear-outline" }', filename: "test.tsx" },
124+
{ code: 'const AVATAR_SIZES = { sm: "w-8 h-8", lg: "w-16 h-16" }', filename: "test.tsx" },
125+
{ code: 'const CARD_IMAGES = { hero: "/images/hero.jpg" }', filename: "test.tsx" },
126+
{ code: 'const ELEMENT_IDS = { header: "main-header", footer: "main-footer" }', filename: "test.tsx" },
127+
// Singular forms
128+
{ code: 'const DEFAULT_COLOR = { value: "#ff0000" }', filename: "test.tsx" },
129+
{ code: 'const MAIN_CLASS = { container: "mx-auto max-w-7xl" }', filename: "test.tsx" },
115130

116131
// Technical strings (no spaces, identifiers)
117132
{ code: 'const x = "myIdentifier"', filename: "test.tsx" },

src/rules/no-unlocalized-strings.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ function isErrorConstructorArgument(
489489
const CAMEL_CASE_PATTERN = /^[a-z]+([A-Z][a-z]+)+$/
490490

491491
/** Technical suffixes with at least one lowercase char before (ensures prefix exists) */
492-
const STYLING_SUFFIX_PATTERN = /[a-z](ClassName|Class|Color|Style|Icon|Size|Id)$/
492+
const STYLING_SUFFIX_PATTERN = /[a-z](ClassName|Class|Color|Style|Icon|Image|Size|Id)$/
493493

494494
/**
495495
* Checks if a property name is a styling/technical property.
@@ -499,6 +499,7 @@ const STYLING_SUFFIX_PATTERN = /[a-z](ClassName|Class|Color|Style|Icon|Size|Id)$
499499
* - "Color": backgroundColor, borderColor, textColor
500500
* - "Style": containerStyle, buttonStyle
501501
* - "Icon": leftIcon, statusIcon
502+
* - "Image": backgroundImage, avatarImage
502503
* - "Size": fontSize, iconSize
503504
* - "Id": containerId, elementId
504505
*
@@ -509,6 +510,28 @@ function isStylingProperty(propertyName: string): boolean {
509510
return CAMEL_CASE_PATTERN.test(propertyName) && STYLING_SUFFIX_PATTERN.test(propertyName)
510511
}
511512

513+
/** UPPER_CASE constant names with styling-related suffixes (singular and plural) */
514+
const UPPER_CASE_STYLING_PATTERN =
515+
/^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*_(CLASSNAMES?|CLASSES?|CLASS|COLORS?|STYLES?|ICONS?|IMAGES?|SIZES?|IDS?)$/
516+
517+
/**
518+
* Checks if a variable name is a styling/technical constant.
519+
*
520+
* Matches UPPER_CASE constants ending with:
521+
* - "_CLASS", "_CLASSES", "_CLASSNAME", "_CLASSNAMES"
522+
* - "_COLOR", "_COLORS"
523+
* - "_STYLE", "_STYLES"
524+
* - "_ICON", "_ICONS"
525+
* - "_IMAGE", "_IMAGES"
526+
* - "_SIZE", "_SIZES"
527+
* - "_ID", "_IDS"
528+
*
529+
* Examples: STATUS_COLORS, BUTTON_CLASSES, THEME_STYLES
530+
*/
531+
function isStylingConstant(variableName: string): boolean {
532+
return UPPER_CASE_STYLING_PATTERN.test(variableName)
533+
}
534+
512535
/**
513536
* Checks if a string is a value for an ignored property/attribute.
514537
*/
@@ -544,6 +567,31 @@ function isIgnoredProperty(node: TSESTree.Node, ignoreProperties: string[]): boo
544567
return false
545568
}
546569

570+
/**
571+
* Checks if a string is inside an object assigned to a styling constant.
572+
*
573+
* Matches patterns like:
574+
* const STATUS_COLORS = { active: "bg-green-100..." }
575+
* const BUTTON_STYLES = { primary: "px-4 py-2..." }
576+
*/
577+
function isInsideStylingConstant(node: TSESTree.Node): boolean {
578+
let current: TSESTree.Node | undefined = node.parent ?? undefined
579+
580+
while (current !== undefined) {
581+
// Look for: const NAME = { ... } or let NAME = { ... }
582+
if (
583+
current.type === AST_NODE_TYPES.VariableDeclarator &&
584+
current.id.type === AST_NODE_TYPES.Identifier &&
585+
isStylingConstant(current.id.name)
586+
) {
587+
return true
588+
}
589+
current = current.parent ?? undefined
590+
}
591+
592+
return false
593+
}
594+
547595
// ============================================================================
548596
// Syntax Context Checks (non-user-facing locations)
549597
// ============================================================================
@@ -886,6 +934,11 @@ export const noUnlocalizedStrings = createRule<[Options], MessageId>({
886934
return
887935
}
888936

937+
// Inside a styling constant (e.g., STATUS_COLORS, BUTTON_CLASSES)
938+
if (isInsideStylingConstant(node)) {
939+
return
940+
}
941+
889942
// TypeScript type-aware: string literal union, Intl API, etc.
890943
if (isTechnicalStringType(node, typeChecker, parserServices)) {
891944
return

0 commit comments

Comments
 (0)