|
1 | 1 | # no-complex-expressions-in-message |
2 | 2 |
|
3 | | -Disallow complex expressions in Lingui messages. |
| 3 | +Disallow complex expressions in Lingui messages — only simple identifiers are allowed. |
4 | 4 |
|
5 | 5 | ## Why? |
6 | 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 |
| 7 | +Complex expressions in translation messages cause problems: |
12 | 8 |
|
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. |
14 | 15 |
|
15 | 16 | ## Rule Details |
16 | 17 |
|
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}` |
18 | 21 |
|
19 | 22 | ### ❌ Invalid |
20 | 23 |
|
21 | 24 | ```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> |
25 | 28 |
|
26 | | -// Non-whitelisted function calls |
| 29 | +// Function calls |
| 30 | +t`Price: ${formatPrice(price)}` |
27 | 31 | t`Random: ${Math.random()}` |
28 | 32 | <Trans>Date: {formatDate(date)}</Trans> |
29 | | -t`Items: ${items.join(', ')}` |
30 | 33 |
|
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> |
33 | 37 |
|
34 | 38 | // Conditional expressions |
35 | | -t`Status: ${isActive ? 'Active' : 'Inactive'}` |
| 39 | +t`Status: ${isActive ? "Active" : "Inactive"}` |
36 | 40 |
|
37 | 41 | // 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 }}` |
39 | 49 | ``` |
40 | 50 |
|
41 | 51 | ### ✅ Valid |
42 | 52 |
|
43 | 53 | ```tsx |
44 | | -// Simple identifiers |
| 54 | +// Simple identifiers only |
45 | 55 | 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> |
47 | 59 |
|
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}` |
52 | 63 |
|
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> |
56 | 66 |
|
57 | | -const formattedDate = formatDate(date) |
58 | | -<Trans>Date: {formattedDate}</Trans> |
59 | | -``` |
| 67 | +const statusText = isActive ? "Active" : "Inactive" |
| 68 | +t`Status: ${statusText}` |
60 | 69 |
|
61 | | -## Options |
62 | | - |
63 | | -### `allowedCallees` |
| 70 | +// Whitespace expressions are allowed in JSX |
| 71 | +<Trans>Hello{" "}{name}</Trans> |
| 72 | +``` |
64 | 73 |
|
65 | | -Array of function names that are allowed. Format: dot-separated strings. |
| 74 | +## Recommended Pattern |
66 | 75 |
|
67 | | -Default: `["i18n.number", "i18n.date"]` |
| 76 | +Always extract expressions to descriptively-named variables: |
68 | 77 |
|
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}!` |
76 | 81 |
|
77 | | -### `allowMemberExpressions` |
| 82 | +// ✅ Good: translator sees "${userName}" |
| 83 | +const userName = user.profile.displayName |
| 84 | +t`Welcome back, ${userName}!` |
78 | 85 |
|
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` |
80 | 88 |
|
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` |
87 | 92 | ``` |
88 | 93 |
|
89 | | -### `maxExpressionDepth` |
| 94 | +## Options |
90 | 95 |
|
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. |
92 | 97 |
|
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 |
96 | 99 |
|
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. |
105 | 101 |
|
106 | 102 | ## When Not To Use It |
107 | 103 |
|
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