Skip to content

Commit 20eb965

Browse files
committed
feat(no-unlocalized-strings): support nested classNames objects
- Add classNames to default ignore properties - Continue up tree past non-styling properties to find parent styling property - Support plural forms for camelCase suffixes (Colors, Styles, Icons, etc.) - Handles patterns like: classNames={{ day: "...", cell: "..." }}
1 parent 65e29d5 commit 20eb965

File tree

3 files changed

+37
-4
lines changed

3 files changed

+37
-4
lines changed

docs/rules/no-unlocalized-strings.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ Default:
138138
```ts
139139
[
140140
"className", // CSS classes - arbitrary strings, always technical
141+
"classNames", // CSS classes object (e.g., for component libraries)
141142
"key", // React key prop
142143
"data-testid" // DOM Testing Library standard
143144
]
@@ -180,9 +181,17 @@ In addition to the explicit list, the rule automatically ignores camelCase prope
180181
<div className={cn("px-4 py-2", "text-white")} />
181182
<div className={clsx("base", condition && "extra")} />
182183
<div className={condition ? "class-a" : "class-b"} />
184+
185+
// Nested classNames objects are fully supported
186+
<Calendar
187+
classNames={{
188+
day: "bg-white text-gray-900",
189+
cell: "p-2 hover:bg-gray-100",
190+
}}
191+
/>
183192
```
184193

185-
This covers common patterns in component libraries like Chakra UI, Material UI, and custom component props.
194+
This covers common patterns in component libraries like Chakra UI, Material UI, react-day-picker, and custom component props.
186195

187196
**Note**: Strings inside callback functions (like `onClick`) are NOT ignored, even when `className` is present on the same element.
188197

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,24 @@ ruleTester.run("no-unlocalized-strings", noUnlocalizedStrings, {
119119
},
120120
// Nested function calls
121121
{ code: '<div className={cn(baseStyles, getVariantClass("primary"))} />', filename: "test.tsx" },
122+
123+
// Nested objects inside styling properties (e.g., classNames prop with sub-properties)
124+
{
125+
code: `<DatePicker
126+
calendarProps={{
127+
className: "dark:bg-slate-800",
128+
classNames: {
129+
caption_label: "dark:text-slate-100",
130+
day: "dark:text-slate--300 dark:hover:bg-slate-700",
131+
nav_button: "dark:text-slate-400",
132+
},
133+
}}
134+
/>`,
135+
filename: "test.tsx"
136+
},
137+
// Plural styling properties
138+
{ code: '<Calendar classNames={{ day: "bg-white", cell: "p-2" }} />', filename: "test.tsx" },
139+
{ code: '({ buttonColors: { primary: "#0000ff", secondary: "#cccccc" } })', filename: "test.tsx" },
122140
// Color properties
123141
{ code: '<Box backgroundColor="#ff0000" />', filename: "test.tsx" },
124142
{ code: '<Text textColor="red-500" />', filename: "test.tsx" },

src/rules/no-unlocalized-strings.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const DEFAULT_IGNORE_FUNCTIONS = ["require", "import"]
3838
const DEFAULT_IGNORE_PROPERTIES = [
3939
// CSS class names - accept arbitrary strings, always technical
4040
"className",
41+
"classNames",
4142
// React key prop
4243
"key",
4344
// Testing ID - DOM Testing Library standard
@@ -489,7 +490,7 @@ function isErrorConstructorArgument(
489490
const CAMEL_CASE_PATTERN = /^[a-z]+([A-Z][a-z]+)+$/
490491

491492
/** Technical suffixes with at least one lowercase char before (ensures prefix exists) */
492-
const STYLING_SUFFIX_PATTERN = /[a-z](ClassName|Class|Color|Style|Icon|Image|Size|Id)$/
493+
const STYLING_SUFFIX_PATTERN = /[a-z](ClassNames?|Class|Colors?|Styles?|Icons?|Images?|Sizes?|Ids?)$/
493494

494495
/**
495496
* Checks if a property name is a styling/technical property.
@@ -578,18 +579,23 @@ function isTechnicalPropertyName(name: string, ignoreProperties: string[]): bool
578579
* className={cn("class1", "class2")}
579580
* className={cn("base", condition && "extra")}
580581
* className={condition ? "a" : "b"}
582+
* classNames={{ day: "text-white", cell: "bg-gray-100" }}
581583
*
582584
* Walks up the tree looking for a JSXAttribute or Property with a styling name.
585+
* Continues past non-styling properties to find parent styling properties.
583586
*/
584587
function isInsideStylingPropertyValue(node: TSESTree.Node, ignoreProperties: string[]): boolean {
585588
let current: TSESTree.Node | undefined = node
586589

587590
while (current !== undefined) {
588591
// Check if current node is directly the value of a styling property
589592
const propName = getPropertyName(current)
590-
if (propName !== null) {
591-
return isTechnicalPropertyName(propName, ignoreProperties)
593+
if (propName !== null && isTechnicalPropertyName(propName, ignoreProperties)) {
594+
// Found a styling property - ignore this string
595+
return true
592596
}
597+
// If we found a non-styling property, continue up the tree
598+
// (the property might be nested inside a styling property like classNames: { day: "..." })
593599

594600
// Stop at function declarations/expressions (don't cross function boundaries)
595601
// This prevents: onClick={() => { return "Hello" }} from being ignored

0 commit comments

Comments
 (0)