Skip to content

Commit a1b174f

Browse files
committed
refactor(no-unlocalized-strings): reduce default ignoreProperties
Leverage TypeScript types to detect technical properties automatically: - Removed type, id, name, href, src, role from defaults (detected via types) - Removed SVG attributes (detected via types or heuristics) - Added SVG path data detection to looksLikeUIString heuristic - Kept only: className, styleName, key, testID, data-testid
1 parent 3e8a9d3 commit a1b174f

File tree

3 files changed

+67
-73
lines changed

3 files changed

+67
-73
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ The `no-unlocalized-strings` rule has different options because TypeScript types
153153
- `useTsTypes: true` — always enabled
154154
- Most `ignoreFunctions` entries for DOM APIs — auto-detected via types
155155
- Most `ignoreNames` entries for typed parameters — auto-detected via types
156+
- Most `ignoreProperties` entries (like `type`, `role`, `href`) — auto-detected via types
156157
- `ignoreMethodsOnTypes` — handled automatically
157158

158159
### Migration Steps

docs/rules/no-unlocalized-strings.md

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,32 +127,30 @@ Wildcard examples:
127127

128128
Array of property/attribute names whose string values should be ignored.
129129

130+
This list is intentionally minimal — most HTML/SVG attributes are detected automatically via TypeScript types (string literal unions like `"button" | "submit"` for `type`).
131+
130132
Default:
131133

132134
```ts
133135
[
134-
// CSS/styling
135-
"className", "styleName", "style",
136-
// HTML attributes
137-
"type", "id", "key", "name", "href", "src", "role",
138-
// Testing
139-
"testID", "data-testid",
140-
// Accessibility
141-
"aria-label", "aria-describedby", "aria-labelledby",
142-
// SVG attributes
143-
"viewBox", "d", "cx", "cy", "r", "x", "y", "width", "height",
144-
"fill", "stroke", "transform", "points", "pathLength"
136+
"className", // CSS classes - arbitrary strings, always technical
137+
"styleName", // CSS modules
138+
"key", // React key prop
139+
"testID", // React Native testing
140+
"data-testid" // DOM testing
145141
]
146142
```
147143

148144
```ts
149145
{
150146
"lingui-ts/no-unlocalized-strings": ["error", {
151-
"ignoreProperties": ["className", "type", "variant", "testId"]
147+
"ignoreProperties": ["className", "testId", "myCustomTechnicalProp"]
152148
}]
153149
}
154150
```
155151

152+
**Note**: Properties like `type`, `role`, `href`, `id` are NOT in the default list because TypeScript automatically detects them as technical when they have string literal union types.
153+
156154
### `ignoreNames`
157155

158156
Array of variable names to ignore.
@@ -192,6 +190,7 @@ The rule uses heuristics to determine if a string looks like UI text:
192190
- URLs and paths (`/`, `https://`, `mailto:`)
193191
- Identifiers without spaces (`myFunction`, `my-css-class`)
194192
- CSS selectors (`:hover`, `.class`, `#id`)
193+
- SVG path data (`M10 10`, `L20 30`)
195194

196195
## When Not To Use It
197196

src/rules/no-unlocalized-strings.ts

Lines changed: 55 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -24,43 +24,23 @@ const DEFAULT_IGNORE_FUNCTIONS = ["console.*", "require", "import", "Error", "Ty
2424

2525
/**
2626
* JSX attributes and object properties whose values should not be checked.
27-
* These are typically technical values, not user-visible text.
27+
*
28+
* This list is intentionally minimal - most HTML/SVG attributes are detected
29+
* automatically via TypeScript types (string literal unions like "button" | "submit").
30+
*
31+
* We only list properties that:
32+
* 1. Accept arbitrary strings (not literal unions) but are still technical
33+
* 2. Are framework-specific and not in standard type definitions
2834
*/
2935
const DEFAULT_IGNORE_PROPERTIES = [
30-
// CSS/styling
36+
// CSS class names - accept arbitrary strings, always technical
3137
"className",
3238
"styleName",
33-
"style",
34-
// HTML attributes
35-
"type",
36-
"id",
39+
// React-specific
3740
"key",
38-
"name",
39-
"href",
40-
"src",
41-
"role",
42-
// Testing
41+
// Testing IDs - arbitrary strings, always technical
4342
"testID",
44-
"data-testid",
45-
// Accessibility (handled by their own rules usually)
46-
"aria-label",
47-
"aria-describedby",
48-
"aria-labelledby",
49-
// SVG attributes
50-
"viewBox",
51-
"d",
52-
"cx",
53-
"cy",
54-
"r",
55-
"x",
56-
"y",
57-
"width",
58-
"height",
59-
"fill",
60-
"stroke",
61-
"transform",
62-
"points",
63-
"pathLength"
43+
"data-testid"
6444
]
6545

6646
/**
@@ -193,6 +173,12 @@ function looksLikeUIString(value: string): boolean {
193173
return false
194174
}
195175

176+
// SVG path data: commands like M, L, C, etc. followed by coordinates
177+
// Examples: "M10 10", "M0 0 L100 100", "M10,10 L20,20"
178+
if (/^[MLHVCSQTAZmlhvcsqtaz][\d\s,.-]+/.test(trimmed)) {
179+
return false
180+
}
181+
196182
// Non-Latin scripts are almost always user-visible text
197183
// Ranges: CJK, Hangul, Cyrillic, Arabic, Hebrew, Thai, Hangul Jamo
198184
if (/[\u3000-\u9fff\uac00-\ud7af\u0400-\u04ff\u0600-\u06ff\u0590-\u05ff\u0e00-\u0e7f\u1100-\u11ff]/.test(trimmed)) {
@@ -448,11 +434,47 @@ function isIntlRelatedType(typeName: string): boolean {
448434
)
449435
}
450436

437+
/**
438+
* Checks if a type is a string literal union (technical type).
439+
*/
440+
function isStringLiteralUnion(type: ts.Type): boolean {
441+
if (type.isUnion()) {
442+
const hasStringLiteral = type.types.some((t) => t.isStringLiteral() || (t.flags & 128) !== 0)
443+
const allTechnical = type.types.every((t) => {
444+
// String literal
445+
if (t.isStringLiteral() || (t.flags & 128) !== 0) {
446+
return true
447+
}
448+
// Number literal
449+
if (t.isNumberLiteral() || (t.flags & 256) !== 0) {
450+
return true
451+
}
452+
// Boolean literals (true/false)
453+
if ((t.flags & 512) !== 0 || (t.flags & 1024) !== 0) {
454+
return true
455+
}
456+
// undefined
457+
if ((t.flags & 32768) !== 0) {
458+
return true
459+
}
460+
// null
461+
if ((t.flags & 65536) !== 0) {
462+
return true
463+
}
464+
return false
465+
})
466+
return hasStringLiteral && allTechnical
467+
}
468+
// Single string literal type
469+
return type.isStringLiteral() || (type.flags & 128) !== 0
470+
}
471+
451472
/**
452473
* Uses TypeScript's type checker to determine if a string is technical.
453474
*
454475
* Detects:
455476
* - String literal union types: type Status = "loading" | "error"
477+
* - JSX attribute types: <input type="text" /> (type is "button" | "checkbox" | ...)
456478
* - Intl API arguments: toLocaleString("en-US", { weekday: "long" })
457479
* - Discriminated union fields: { type: "add" } | { type: "remove" }
458480
*
@@ -471,36 +493,8 @@ function isTechnicalStringType(
471493

472494
if (contextualType !== undefined) {
473495
// Check for string literal unions: "a" | "b" | "c"
474-
// Also allow unions with undefined, null, boolean, number
475-
if (contextualType.isUnion()) {
476-
const hasStringLiteral = contextualType.types.some((t) => t.isStringLiteral() || (t.flags & 128) !== 0)
477-
const allTechnical = contextualType.types.every((t) => {
478-
// String literal
479-
if (t.isStringLiteral() || (t.flags & 128) !== 0) {
480-
return true
481-
}
482-
// Number literal
483-
if (t.isNumberLiteral() || (t.flags & 256) !== 0) {
484-
return true
485-
}
486-
// Boolean literals (true/false)
487-
if ((t.flags & 512) !== 0 || (t.flags & 1024) !== 0) {
488-
return true
489-
}
490-
// undefined
491-
if ((t.flags & 32768) !== 0) {
492-
return true
493-
}
494-
// null
495-
if ((t.flags & 65536) !== 0) {
496-
return true
497-
}
498-
return false
499-
})
500-
501-
if (hasStringLiteral && allTechnical) {
502-
return true
503-
}
496+
if (isStringLiteralUnion(contextualType)) {
497+
return true
504498
}
505499

506500
// Check for Intl API types

0 commit comments

Comments
 (0)