Skip to content

Commit 573e3ae

Browse files
committed
docs: align rule documentation with actual implementation
- consistent-plural-format: document style option (hash/template) - no-complex-expressions: remove non-existent options - no-unlocalized-strings: fix default ignoreFunctions values - text-restrictions: document rules array format - valid-t-call-location: document class properties as allowed - README: add migration guide from eslint-plugin-lingui
1 parent bf6eede commit 573e3ae

File tree

6 files changed

+321
-147
lines changed

6 files changed

+321
-147
lines changed

README.md

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,75 @@ Or configure rules manually:
9191

9292
| Rule | Description | Recommended |
9393
|------|-------------|:-----------:|
94-
| [no-unlocalized-strings](docs/rules/no-unlocalized-strings.md) | Detects user-visible strings not wrapped in Lingui macros. Uses TypeScript types to automatically ignore technical strings like string literal unions, DOM APIs, Intl methods, and discriminated union fields. ||
95-
| [no-single-variable-message](docs/rules/no-single-variable-message.md) | Disallows messages that consist only of a single variable without surrounding text. Such messages provide no context for translators. ||
96-
| [no-single-tag-message](docs/rules/no-single-tag-message.md) | Disallows `<Trans>` components that contain only a single JSX element without text. The wrapped element should be translated directly instead. ||
94+
| [no-unlocalized-strings](docs/rules/no-unlocalized-strings.md) | Detects user-visible strings not wrapped in Lingui macros. Uses TypeScript types to automatically ignore technical strings like string literal unions, DOM APIs, and Intl methods. ||
95+
| [no-single-variable-message](docs/rules/no-single-variable-message.md) | Disallows messages that consist only of variables without surrounding text. Such messages provide no context for translators. ||
96+
| [no-single-tag-message](docs/rules/no-single-tag-message.md) | Disallows `<Trans>` components that contain only a single JSX element without text. The wrapped element should have surrounding text for context. ||
9797
| [no-nested-macros](docs/rules/no-nested-macros.md) | Prevents nesting Lingui macros inside each other (e.g., `t` inside `<Trans>`). Nested macros create invalid message catalogs and confuse translators. ||
98-
| [no-complex-expressions-in-message](docs/rules/no-complex-expressions-in-message.md) | Restricts embedded expressions in messages to simple identifiers and member access. Complex expressions like function calls or ternaries should be extracted to variables. ||
99-
| [valid-t-call-location](docs/rules/valid-t-call-location.md) | Ensures `t` macro calls are inside functions, not at module scope. Module-level calls execute before i18n is initialized and won't update on locale change. ||
100-
| [consistent-plural-format](docs/rules/consistent-plural-format.md) | Validates `<Plural>` component usage by ensuring required plural keys (`one`, `other`) are present. Helps maintain consistent pluralization across the codebase. ||
101-
| [text-restrictions](docs/rules/text-restrictions.md) | Enforces project-specific text restrictions like disallowed patterns or minimum length. Requires configuration to be useful. ||
98+
| [no-complex-expressions-in-message](docs/rules/no-complex-expressions-in-message.md) | Restricts embedded expressions to simple identifiers only. Complex expressions like `${user.name}` or `${formatPrice(x)}` must be extracted to named variables first. ||
99+
| [valid-t-call-location](docs/rules/valid-t-call-location.md) | Ensures `t` macro calls are inside functions or class properties, not at module scope. Module-level calls execute before i18n is initialized and won't update on locale change. ||
100+
| [consistent-plural-format](docs/rules/consistent-plural-format.md) | Enforces consistent plural value format — either `#` hash syntax or `${var}` template literals throughout the codebase. ||
101+
| [text-restrictions](docs/rules/text-restrictions.md) | Enforces project-specific text restrictions with custom patterns and messages. Requires configuration. ||
102+
103+
## Migrating from eslint-plugin-lingui
104+
105+
This plugin is a TypeScript-focused alternative to the official [eslint-plugin-lingui](https://github.com/lingui/eslint-plugin-lingui). Here are the key differences:
106+
107+
### Key Differences
108+
109+
| Feature | eslint-plugin-lingui | eslint-plugin-lingui-typescript |
110+
|---------|---------------------|--------------------------------|
111+
| **Type-aware detection** | ❌ Heuristics only | ✅ Uses TypeScript types |
112+
| **String literal unions** | Manual whitelist | ✅ Auto-detected |
113+
| **DOM API strings** | Manual whitelist | ✅ Auto-detected |
114+
| **Intl method arguments** | Manual whitelist | ✅ Auto-detected |
115+
| **ESLint version** | 8.x | 9.x (flat config) |
116+
| **Config format** | Legacy `.eslintrc` | Flat config only |
117+
118+
### Why Switch?
119+
120+
1. **Less configuration**: TypeScript's type system automatically identifies technical strings — no need to maintain long whitelists of ignored functions and patterns.
121+
122+
2. **Fewer false positives**: Strings typed as literal unions (like `"loading" | "error"`) are automatically recognized as non-translatable.
123+
124+
3. **Modern ESLint**: Built for ESLint 9's flat config from the ground up.
125+
126+
### Rule Mapping
127+
128+
| eslint-plugin-lingui | eslint-plugin-lingui-typescript |
129+
|---------------------|--------------------------------|
130+
| `lingui/no-unlocalized-strings` | `lingui-ts/no-unlocalized-strings` |
131+
| `lingui/t-call-in-function` | `lingui-ts/valid-t-call-location` |
132+
| `lingui/no-single-variables-to-translate` | `lingui-ts/no-single-variable-message` |
133+
| `lingui/no-expression-in-message` | `lingui-ts/no-complex-expressions-in-message` |
134+
| `lingui/no-single-tag-to-translate` | `lingui-ts/no-single-tag-message` |
135+
| `lingui/text-restrictions` | `lingui-ts/text-restrictions` |
136+
|| `lingui-ts/no-nested-macros` (new) |
137+
|| `lingui-ts/consistent-plural-format` (new) |
138+
139+
### Migration Steps
140+
141+
1. Remove the old plugin:
142+
```bash
143+
npm uninstall eslint-plugin-lingui
144+
```
145+
146+
2. Install this plugin:
147+
```bash
148+
npm install --save-dev eslint-plugin-lingui-typescript
149+
```
150+
151+
3. Update your ESLint config to flat config format (if not already):
152+
```ts
153+
// eslint.config.ts
154+
import linguiPlugin from "eslint-plugin-lingui-typescript"
155+
156+
export default [
157+
// ... other configs
158+
linguiPlugin.configs["flat/recommended"]
159+
]
160+
```
161+
162+
4. Review your ignore lists — many entries may no longer be needed thanks to type-aware detection.
102163

103164
## Related Projects
104165

Lines changed: 61 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,97 @@
11
# consistent-plural-format
22

3-
Ensure `<Plural>` component has required plural category props.
3+
Enforce consistent plural format style (`#` hash or `${var}` template).
44

55
## Why?
66

7-
Proper pluralization requires specific props for different quantities. Missing props can cause:
8-
- Runtime errors or fallback to incorrect text
9-
- Incomplete translations
10-
- Poor user experience for different languages
7+
Lingui supports two formats for interpolating the count value in plural messages:
8+
9+
1. **Hash format**: `"# items"` — uses `#` as a placeholder
10+
2. **Template format**: `` `${count} items` `` — uses template literals
11+
12+
Mixing both styles in a codebase leads to inconsistency and confusion. This rule enforces one style throughout your project.
1113

1214
## Rule Details
1315

14-
This rule ensures that `<Plural>` components include all required plural category props.
16+
This rule checks `plural()` calls and `<Plural>` components and reports when the wrong format style is used.
1517

16-
### ❌ Invalid
18+
### With `style: "hash"` (default)
19+
20+
#### ❌ Invalid
1721

1822
```tsx
19-
// Missing 'other' (required by default)
20-
<Plural value={count} one="# item" />
23+
// Template format when hash is required
24+
plural(count, {
25+
one: `${count} item`,
26+
other: `${count} items`
27+
})
28+
29+
<Plural
30+
value={count}
31+
one={`${count} item`}
32+
other={`${count} items`}
33+
/>
34+
```
2135

22-
// Missing 'one' (required by default)
23-
<Plural value={count} other="# items" />
36+
#### ✅ Valid
2437

25-
// Missing both required props
26-
<Plural value={count} zero="None" />
38+
```tsx
39+
// Hash format
40+
plural(count, {
41+
one: "# item",
42+
other: "# items"
43+
})
44+
45+
<Plural
46+
value={count}
47+
one="# item"
48+
other="# items"
49+
/>
2750
```
2851

29-
### ✅ Valid
52+
### With `style: "template"`
53+
54+
#### ❌ Invalid
3055

3156
```tsx
32-
// All required props present
33-
<Plural value={count} one="# item" other="# items" />
57+
// Hash format when template is required
58+
plural(count, {
59+
one: "# item",
60+
other: "# items"
61+
})
62+
```
3463

35-
// Additional props are allowed
36-
<Plural value={count} one="One" other="Many" zero="None" />
64+
#### ✅ Valid
3765

38-
// With expressions
39-
<Plural value={count} one={oneMsg} other={otherMsg} />
66+
```tsx
67+
// Template format
68+
plural(count, {
69+
one: `${count} item`,
70+
other: `${count} items`
71+
})
4072
```
4173

4274
## Options
4375

44-
### `requiredKeys`
76+
### `style`
4577

46-
Array of plural category props that must be present. Default: `["one", "other"]`
78+
Which format style to enforce. Default: `"hash"`
4779

48-
Common CLDR plural categories: `zero`, `one`, `two`, `few`, `many`, `other`
80+
- `"hash"` — Require `#` placeholder (Lingui's standard format)
81+
- `"template"` — Require `${var}` template literals
4982

5083
```ts
51-
// Require only 'other'
84+
// Enforce hash format (default)
5285
{
53-
"lingui-ts/consistent-plural-format": ["error", {
54-
"requiredKeys": ["other"]
55-
}]
86+
"lingui-ts/consistent-plural-format": ["error", { "style": "hash" }]
5687
}
5788

58-
// Require zero, one, and other
89+
// Enforce template format
5990
{
60-
"lingui-ts/consistent-plural-format": ["error", {
61-
"requiredKeys": ["zero", "one", "other"]
62-
}]
91+
"lingui-ts/consistent-plural-format": ["error", { "style": "template" }]
6392
}
6493
```
6594

6695
## When Not To Use It
6796

68-
If your project uses ICU message format directly in `t` strings instead of `<Plural>` components, this rule won't help. It only checks JSX `<Plural>` components.
97+
If your project intentionally mixes both formats or you don't use `plural()` / `<Plural>`, you can disable this rule.
Lines changed: 59 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,104 @@
11
# no-complex-expressions-in-message
22

3-
Disallow complex expressions in Lingui messages.
3+
Disallow complex expressions in Lingui messages — only simple identifiers are allowed.
44

55
## Why?
66

7-
Complex expressions in translation messages:
8-
- Make it harder for translators to understand the context
9-
- Can cause issues with message extraction
10-
- May lead to runtime errors if the expression fails
11-
- Make the code harder to maintain
7+
Complex expressions in translation messages cause problems:
128

13-
Extract complex logic to variables before using them in messages.
9+
- **Translators can't understand code**: Expressions like `${user.name}` or `${formatPrice(price)}` are meaningless to translators
10+
- **Missing context**: Translators need descriptive placeholder names, not code
11+
- **Extraction issues**: Complex expressions can cause problems with message extraction tools
12+
- **Maintenance burden**: Inline logic is harder to test and maintain
13+
14+
The solution is simple: extract any complex expression to a named variable first.
1415

1516
## Rule Details
1617

17-
This rule reports complex expressions inside `t` tagged templates and `<Trans>` components.
18+
This rule reports any expression inside `t` tagged templates or `<Trans>` components that is not a simple identifier.
19+
20+
**Only simple identifiers are allowed**: `${name}`, `${count}`, `${formattedPrice}`
1821

1922
### ❌ Invalid
2023

2124
```tsx
22-
// Binary/arithmetic expressions
23-
t`Price: ${price * 1.2}`
24-
<Trans>Total: {count + 1}</Trans>
25+
// Member expressions
26+
t`Hello ${user.name}`
27+
<Trans>Hello {user.name}</Trans>
2528

26-
// Non-whitelisted function calls
29+
// Function calls
30+
t`Price: ${formatPrice(price)}`
2731
t`Random: ${Math.random()}`
2832
<Trans>Date: {formatDate(date)}</Trans>
29-
t`Items: ${items.join(', ')}`
3033

31-
// Member expressions (by default)
32-
t`Hello ${user.name}`
34+
// Binary expressions
35+
t`Total: ${price * 1.2}`
36+
<Trans>Sum: {a + b}</Trans>
3337

3438
// Conditional expressions
35-
t`Status: ${isActive ? 'Active' : 'Inactive'}`
39+
t`Status: ${isActive ? "Active" : "Inactive"}`
3640

3741
// Logical expressions
38-
t`Name: ${name || 'Unknown'}`
42+
t`Name: ${name || "Unknown"}`
43+
44+
// Template literals inside expressions
45+
t`Result: ${`nested ${value}`}`
46+
47+
// Legacy placeholder syntax
48+
t`Hello ${{ name: userName }}`
3949
```
4050

4151
### ✅ Valid
4252

4353
```tsx
44-
// Simple identifiers
54+
// Simple identifiers only
4555
t`Hello ${name}`
46-
<Trans>You have {count} items</Trans>
56+
t`You have ${count} items`
57+
<Trans>Hello {name}</Trans>
58+
<Trans>Total: {formattedTotal}</Trans>
4759

48-
// Whitelisted Lingui helpers
49-
t`Price: ${i18n.number(price)}`
50-
t`Date: ${i18n.date(date)}`
51-
<Trans>Price: {i18n.number(price)}</Trans>
60+
// Extract complex expressions first
61+
const displayName = user.name
62+
t`Hello ${displayName}`
5263

53-
// Extract complex logic first
54-
const displayPrice = price * 1.2
55-
t`Price: ${displayPrice}`
64+
const formattedPrice = formatPrice(price)
65+
<Trans>Price: {formattedPrice}</Trans>
5666

57-
const formattedDate = formatDate(date)
58-
<Trans>Date: {formattedDate}</Trans>
59-
```
67+
const statusText = isActive ? "Active" : "Inactive"
68+
t`Status: ${statusText}`
6069

61-
## Options
62-
63-
### `allowedCallees`
70+
// Whitespace expressions are allowed in JSX
71+
<Trans>Hello{" "}{name}</Trans>
72+
```
6473

65-
Array of function names that are allowed. Format: dot-separated strings.
74+
## Recommended Pattern
6675

67-
Default: `["i18n.number", "i18n.date"]`
76+
Always extract expressions to descriptively-named variables:
6877

69-
```ts
70-
{
71-
"lingui-ts/no-complex-expressions-in-message": ["error", {
72-
"allowedCallees": ["i18n.number", "i18n.date", "formatCurrency"]
73-
}]
74-
}
75-
```
78+
```tsx
79+
// ❌ Bad: translator sees "${user.profile.displayName}"
80+
t`Welcome back, ${user.profile.displayName}!`
7681

77-
### `allowMemberExpressions`
82+
// ✅ Good: translator sees "${userName}"
83+
const userName = user.profile.displayName
84+
t`Welcome back, ${userName}!`
7885

79-
Whether to allow simple member expressions like `props.name`. Default: `false`
86+
// ❌ Bad: translator sees "${items.filter(...).length}"
87+
t`${items.filter(i => i.active).length} active items`
8088

81-
```ts
82-
{
83-
"lingui-ts/no-complex-expressions-in-message": ["error", {
84-
"allowMemberExpressions": true
85-
}]
86-
}
89+
// ✅ Good: translator sees "${activeCount}"
90+
const activeCount = items.filter(i => i.active).length
91+
t`${activeCount} active items`
8792
```
8893

89-
### `maxExpressionDepth`
94+
## Options
9095

91-
Maximum depth for member expression chains when `allowMemberExpressions` is `true`. Default: `1`
96+
This rule has no options. All non-identifier expressions are disallowed.
9297

93-
- `1`: allows `user.name` but not `user.address.street`
94-
- `2`: allows up to `user.address.street`
95-
- `null`: no limit
98+
## Note on Lingui Helpers
9699

97-
```ts
98-
{
99-
"lingui-ts/no-complex-expressions-in-message": ["error", {
100-
"allowMemberExpressions": true,
101-
"maxExpressionDepth": 2
102-
}]
103-
}
104-
```
100+
Unlike some other i18n libraries, Lingui's `plural()`, `select()`, and `selectOrdinal()` should **not** be nested inside `t` templates. Use the JSX components `<Plural>`, `<Select>`, `<SelectOrdinal>` instead, or ICU message syntax in your translation catalog.
105101

106102
## When Not To Use It
107103

108-
If your codebase has established patterns that rely on inline expressions and you handle translation complexity elsewhere, you can disable this rule.
109-
104+
If your codebase has established patterns that rely heavily on inline expressions and you handle translation complexity elsewhere, you can disable this rule. However, consider the impact on translator experience.

0 commit comments

Comments
 (0)