Skip to content

Commit 5425591

Browse files
committed
feat(rules): add no-complex-expressions-in-message rule
Disallows complex expressions (binary ops, function calls, conditionals) in Lingui messages. Configurable via: - allowedCallees: whitelist function names (default: i18n helpers) - allowMemberExpressions: allow props.name style access - maxExpressionDepth: limit member chain depth
1 parent 737c6f8 commit 5425591

File tree

5 files changed

+483
-1
lines changed

5 files changed

+483
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ export default [
5252

5353
| Rule | Description | Recommended |
5454
|------|-------------|:-----------:|
55+
| [no-complex-expressions-in-message](docs/rules/no-complex-expressions-in-message.md) | Restrict complexity of expressions in messages ||
5556
| [no-nested-macros](docs/rules/no-nested-macros.md) | Disallow nesting Lingui macros ||
5657
| [no-single-tag-message](docs/rules/no-single-tag-message.md) | Disallow messages with only a single markup tag ||
5758
| [no-single-variable-message](docs/rules/no-single-variable-message.md) | Disallow messages that consist only of a single variable ||
5859

5960
### Planned Rules
6061

61-
- `no-complex-expressions-in-message` — Restrict complexity of expressions in messages
6262
- `valid-t-call-location` — Enforce valid locations for `t` macro calls
6363
- `no-unlocalized-strings` — Detect user-visible strings not wrapped in Lingui
6464
- `text-restrictions` — Enforce project-specific text restrictions
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# no-complex-expressions-in-message
2+
3+
Disallow complex expressions in Lingui messages.
4+
5+
## Why?
6+
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
12+
13+
Extract complex logic to variables before using them in messages.
14+
15+
## Rule Details
16+
17+
This rule reports complex expressions inside `t` tagged templates and `<Trans>` components.
18+
19+
### ❌ Invalid
20+
21+
```tsx
22+
// Binary/arithmetic expressions
23+
t`Price: ${price * 1.2}`
24+
<Trans>Total: {count + 1}</Trans>
25+
26+
// Non-whitelisted function calls
27+
t`Random: ${Math.random()}`
28+
<Trans>Date: {formatDate(date)}</Trans>
29+
t`Items: ${items.join(', ')}`
30+
31+
// Member expressions (by default)
32+
t`Hello ${user.name}`
33+
34+
// Conditional expressions
35+
t`Status: ${isActive ? 'Active' : 'Inactive'}`
36+
37+
// Logical expressions
38+
t`Name: ${name || 'Unknown'}`
39+
```
40+
41+
### ✅ Valid
42+
43+
```tsx
44+
// Simple identifiers
45+
t`Hello ${name}`
46+
<Trans>You have {count} items</Trans>
47+
48+
// Whitelisted Lingui helpers
49+
t`Price: ${i18n.number(price)}`
50+
t`Date: ${i18n.date(date)}`
51+
<Trans>Items: {i18n.plural(count, { one: '#', other: '#' })}</Trans>
52+
53+
// Extract complex logic first
54+
const displayPrice = price * 1.2
55+
t`Price: ${displayPrice}`
56+
57+
const formattedDate = formatDate(date)
58+
<Trans>Date: {formattedDate}</Trans>
59+
```
60+
61+
## Options
62+
63+
### `allowedCallees`
64+
65+
Array of function names that are allowed. Format: dot-separated strings.
66+
67+
Default: `["i18n.number", "i18n.date", "i18n.plural", "i18n.select"]`
68+
69+
```ts
70+
{
71+
"lingui-ts/no-complex-expressions-in-message": ["error", {
72+
"allowedCallees": ["i18n.number", "i18n.date", "formatCurrency"]
73+
}]
74+
}
75+
```
76+
77+
### `allowMemberExpressions`
78+
79+
Whether to allow simple member expressions like `props.name`. Default: `false`
80+
81+
```ts
82+
{
83+
"lingui-ts/no-complex-expressions-in-message": ["error", {
84+
"allowMemberExpressions": true
85+
}]
86+
}
87+
```
88+
89+
### `maxExpressionDepth`
90+
91+
Maximum depth for member expression chains when `allowMemberExpressions` is `true`. Default: `1`
92+
93+
- `1`: allows `user.name` but not `user.address.street`
94+
- `2`: allows up to `user.address.street`
95+
- `null`: no limit
96+
97+
```ts
98+
{
99+
"lingui-ts/no-complex-expressions-in-message": ["error", {
100+
"allowMemberExpressions": true,
101+
"maxExpressionDepth": 2
102+
}]
103+
}
104+
```
105+
106+
## When Not To Use It
107+
108+
If your codebase has established patterns that rely on inline expressions and you handle translation complexity elsewhere, you can disable this rule.
109+

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @packageDocumentation
55
*/
66

7+
import { noComplexExpressionsInMessage } from "./rules/no-complex-expressions-in-message.js"
78
import { noNestedMacros } from "./rules/no-nested-macros.js"
89
import { noSingleTagMessage } from "./rules/no-single-tag-message.js"
910
import { noSingleVariableMessage } from "./rules/no-single-variable-message.js"
@@ -14,6 +15,7 @@ const plugin = {
1415
version: "1.0.0"
1516
},
1617
rules: {
18+
"no-complex-expressions-in-message": noComplexExpressionsInMessage,
1719
"no-nested-macros": noNestedMacros,
1820
"no-single-tag-message": noSingleTagMessage,
1921
"no-single-variable-message": noSingleVariableMessage
@@ -28,6 +30,7 @@ plugin.configs = {
2830
"lingui-ts": plugin
2931
},
3032
rules: {
33+
"lingui-ts/no-complex-expressions-in-message": "error",
3134
"lingui-ts/no-nested-macros": "error",
3235
"lingui-ts/no-single-tag-message": "error",
3336
"lingui-ts/no-single-variable-message": "error"
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { RuleTester } from "@typescript-eslint/rule-tester"
2+
import { afterAll, describe, it } from "vitest"
3+
4+
import { noComplexExpressionsInMessage } from "./no-complex-expressions-in-message.js"
5+
6+
RuleTester.afterAll = afterAll
7+
RuleTester.describe = describe
8+
RuleTester.it = it
9+
10+
const ruleTester = new RuleTester({
11+
languageOptions: {
12+
parserOptions: {
13+
ecmaVersion: 2022,
14+
sourceType: "module",
15+
ecmaFeatures: {
16+
jsx: true
17+
}
18+
}
19+
}
20+
})
21+
22+
ruleTester.run("no-complex-expressions-in-message", noComplexExpressionsInMessage, {
23+
valid: [
24+
// Simple identifiers in t``
25+
"t`Hello ${name}`",
26+
"t`You have ${count} items`",
27+
"t`${greeting}, ${name}!`",
28+
29+
// Simple identifiers in <Trans>
30+
"<Trans>Hello {name}</Trans>",
31+
"<Trans>You have {count} items</Trans>",
32+
33+
// Allowed callees (default: i18n.number, i18n.date, etc.)
34+
"t`Price: ${i18n.number(price)}`",
35+
"t`Date: ${i18n.date(date)}`",
36+
"<Trans>Price: {i18n.number(price)}</Trans>",
37+
"<Trans>Items: {i18n.plural(count, { one: '#', other: '#' })}</Trans>",
38+
39+
// Member expressions when allowed
40+
{
41+
code: "t`Hello ${props.name}`",
42+
options: [{ allowMemberExpressions: true, allowedCallees: [], maxExpressionDepth: 1 }]
43+
},
44+
{
45+
code: "<Trans>Hello {user.name}</Trans>",
46+
options: [{ allowMemberExpressions: true, allowedCallees: [], maxExpressionDepth: 1 }]
47+
},
48+
49+
// Non-Lingui tagged templates should be ignored
50+
"css`color: ${theme.primary}`",
51+
"html`<div>${content}</div>`",
52+
53+
// Non-Trans JSX elements should be ignored
54+
"<div>{Math.random()}</div>",
55+
"<Plural>{count * 2}</Plural>"
56+
],
57+
invalid: [
58+
// Binary expressions
59+
{
60+
code: "t`Price: ${price * 1.2}`",
61+
errors: [{ messageId: "complexExpression" }]
62+
},
63+
{
64+
code: "<Trans>Total: {count + 1}</Trans>",
65+
errors: [{ messageId: "complexExpression" }]
66+
},
67+
68+
// Non-whitelisted function calls
69+
{
70+
code: "t`Random: ${Math.random()}`",
71+
errors: [{ messageId: "complexExpression" }]
72+
},
73+
{
74+
code: "<Trans>Date: {formatDate(date)}</Trans>",
75+
errors: [{ messageId: "complexExpression" }]
76+
},
77+
{
78+
code: "t`Items: ${items.join(', ')}`",
79+
errors: [{ messageId: "complexExpression" }]
80+
},
81+
82+
// Member expressions when not allowed (default)
83+
{
84+
code: "t`Hello ${user.name}`",
85+
errors: [{ messageId: "complexExpression" }]
86+
},
87+
{
88+
code: "<Trans>Hello {props.value}</Trans>",
89+
errors: [{ messageId: "complexExpression" }]
90+
},
91+
92+
// Deep member expressions even when allowed
93+
{
94+
code: "t`Street: ${user.address.street}`",
95+
options: [{ allowMemberExpressions: true, allowedCallees: [], maxExpressionDepth: 1 }],
96+
errors: [{ messageId: "complexExpression" }]
97+
},
98+
99+
// Optional chaining
100+
{
101+
code: "t`Name: ${user?.name}`",
102+
options: [{ allowMemberExpressions: true, allowedCallees: [], maxExpressionDepth: 1 }],
103+
errors: [{ messageId: "complexExpression" }]
104+
},
105+
106+
// Conditional expressions
107+
{
108+
code: "t`Status: ${isActive ? 'Active' : 'Inactive'}`",
109+
errors: [{ messageId: "complexExpression" }]
110+
},
111+
{
112+
code: "<Trans>Status: {isActive ? 'on' : 'off'}</Trans>",
113+
errors: [{ messageId: "complexExpression" }]
114+
},
115+
116+
// Logical expressions
117+
{
118+
code: "t`Name: ${name || 'Unknown'}`",
119+
errors: [{ messageId: "complexExpression" }]
120+
},
121+
122+
// Template literals inside
123+
{
124+
code: "t`Value: ${`nested ${x}`}`",
125+
errors: [{ messageId: "complexExpression" }]
126+
},
127+
128+
// Multiple violations in one message
129+
{
130+
code: "t`${a + b} and ${Math.random()}`",
131+
errors: [
132+
{ messageId: "complexExpression" },
133+
{ messageId: "complexExpression" }
134+
]
135+
},
136+
{
137+
code: "<Trans>{x * 2} plus {y * 3}</Trans>",
138+
errors: [
139+
{ messageId: "complexExpression" },
140+
{ messageId: "complexExpression" }
141+
]
142+
}
143+
]
144+
})
145+

0 commit comments

Comments
 (0)