eslint-plugin-lingui-typescript 1.8.5 → 1.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -127
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/rules/no-unlocalized-strings.d.ts.map +1 -1
- package/dist/rules/no-unlocalized-strings.js +206 -6
- package/dist/rules/no-unlocalized-strings.js.map +1 -1
- package/dist/rules/no-unlocalized-strings.test.js +69 -0
- package/dist/rules/no-unlocalized-strings.test.js.map +1 -1
- package/package.json +5 -5
- package/dist/rules/no-complex-expressions-in-message.d.ts +0 -7
- package/dist/rules/no-complex-expressions-in-message.d.ts.map +0 -1
- package/dist/rules/no-complex-expressions-in-message.js +0 -184
- package/dist/rules/no-complex-expressions-in-message.js.map +0 -1
- package/dist/rules/no-complex-expressions-in-message.test.d.ts +0 -2
- package/dist/rules/no-complex-expressions-in-message.test.d.ts.map +0 -1
- package/dist/rules/no-complex-expressions-in-message.test.js +0 -122
- package/dist/rules/no-complex-expressions-in-message.test.js.map +0 -1
- package/dist/rules/no-single-tag-message.d.ts +0 -2
- package/dist/rules/no-single-tag-message.d.ts.map +0 -1
- package/dist/rules/no-single-tag-message.js +0 -49
- package/dist/rules/no-single-tag-message.js.map +0 -1
- package/dist/rules/no-single-tag-message.test.d.ts +0 -2
- package/dist/rules/no-single-tag-message.test.d.ts.map +0 -1
- package/dist/rules/no-single-tag-message.test.js +0 -72
- package/dist/rules/no-single-tag-message.test.js.map +0 -1
- package/dist/rules/no-single-variable-message.d.ts +0 -2
- package/dist/rules/no-single-variable-message.d.ts.map +0 -1
- package/dist/rules/no-single-variable-message.js +0 -74
- package/dist/rules/no-single-variable-message.js.map +0 -1
- package/dist/rules/no-single-variable-message.test.d.ts +0 -2
- package/dist/rules/no-single-variable-message.test.d.ts.map +0 -1
- package/dist/rules/no-single-variable-message.test.js +0 -74
- package/dist/rules/no-single-variable-message.test.js.map +0 -1
- package/dist/rules/valid-t-call-location.d.ts +0 -5
- package/dist/rules/valid-t-call-location.d.ts.map +0 -1
- package/dist/rules/valid-t-call-location.js +0 -69
- package/dist/rules/valid-t-call-location.js.map +0 -1
- package/dist/rules/valid-t-call-location.test.d.ts +0 -2
- package/dist/rules/valid-t-call-location.test.d.ts.map +0 -1
- package/dist/rules/valid-t-call-location.test.js +0 -104
- package/dist/rules/valid-t-call-location.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,119 +5,77 @@
|
|
|
5
5
|
[](https://github.com/sebastian-software/eslint-plugin-lingui-typescript/actions/workflows/ci.yml)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
ESLint rules for [Lingui](https://lingui.dev/) that use TypeScript's type system to tell technical strings from user-facing text. No whitelists to maintain. No false positives to suppress. No configuration to tweak.
|
|
9
9
|
|
|
10
|
-
**[
|
|
10
|
+
**[Documentation & Examples](https://sebastian-software.github.io/eslint-plugin-lingui-typescript/)**
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## The problem with pattern-based i18n linting
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
i18n linters that rely on pattern matching can't tell a CSS class name from a button label, or a DOM event name from an error message. You end up maintaining long ignore lists and still get false positives every time someone calls a new API:
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
```ts
|
|
17
|
+
// Pattern-based linters flag all of these as "missing translation"
|
|
18
|
+
document.createElement("div") // It's a DOM tag name
|
|
19
|
+
element.addEventListener("click", handler) // It's an event name
|
|
20
|
+
fetch(url, { mode: "cors" }) // It's a typed option
|
|
21
|
+
const status: "idle" | "loading" = "idle" // It's a string literal union
|
|
22
|
+
<Box className="flex items-center" /> // It's a CSS class
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
You add each one to a whitelist. The whitelist grows. New team members hit the same false positives all over again.
|
|
26
|
+
|
|
27
|
+
## How this plugin solves it
|
|
28
|
+
|
|
29
|
+
This plugin reads TypeScript's type information instead of guessing. When a string flows into a parameter typed as `keyof HTMLElementTagNameMap`, or is assigned to a variable typed as `"idle" | "loading" | "error"`, the plugin knows it's technical. You don't configure anything.
|
|
17
30
|
|
|
18
31
|
```ts
|
|
19
|
-
//
|
|
20
|
-
document.createElement("div")
|
|
21
|
-
element.addEventListener("click", handler)
|
|
22
|
-
fetch(url, { mode: "cors" })
|
|
32
|
+
// Automatically ignored — TypeScript provides the context
|
|
33
|
+
document.createElement("div") // keyof HTMLElementTagNameMap
|
|
34
|
+
element.addEventListener("click", handler) // keyof GlobalEventHandlersEventMap
|
|
35
|
+
fetch(url, { mode: "cors" }) // RequestMode
|
|
23
36
|
date.toLocaleDateString("de-DE", { weekday: "long" }) // Intl.DateTimeFormatOptions
|
|
24
37
|
|
|
25
38
|
type Status = "idle" | "loading" | "error"
|
|
26
|
-
const status: Status = "loading"
|
|
39
|
+
const status: Status = "loading" // String literal union
|
|
27
40
|
|
|
28
|
-
//
|
|
29
|
-
<Box containerClassName="flex items-center" />
|
|
30
|
-
<div className={clsx("px-4", "py-2")} />
|
|
31
|
-
<Calendar classNames={{ day: "bg-white" }} />
|
|
32
|
-
const colorClasses = { active: "bg-green-100" }
|
|
33
|
-
const price = "1,00€"
|
|
41
|
+
// Styling props and utility patterns — recognized automatically
|
|
42
|
+
<Box containerClassName="flex items-center" /> // *ClassName, *Color, *Style
|
|
43
|
+
<div className={clsx("px-4", "py-2")} /> // className utilities (clsx, cn)
|
|
44
|
+
<Calendar classNames={{ day: "bg-white" }} /> // Nested classNames objects
|
|
45
|
+
const colorClasses = { active: "bg-green-100" } // *Classes, *Colors, *Styles
|
|
46
|
+
const price = "1,00€" // No letters = not user-facing
|
|
34
47
|
|
|
35
|
-
//
|
|
48
|
+
// Reported — these actually need translation
|
|
36
49
|
const message = "Welcome to our app"
|
|
37
50
|
<button>Save changes</button>
|
|
38
51
|
```
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
### Smart Lingui Detection
|
|
43
|
-
|
|
44
|
-
The plugin uses TypeScript's symbol resolution to verify that `t`, `Trans`, `msg`, etc. actually come from Lingui packages — not just any function with the same name:
|
|
53
|
+
The plugin also verifies that `t`, `Trans`, and other macros actually come from `@lingui/*` packages through TypeScript's symbol resolution, not just name matching:
|
|
45
54
|
|
|
46
55
|
```ts
|
|
47
56
|
import { t } from "@lingui/macro"
|
|
48
|
-
const label = t`Save`
|
|
57
|
+
const label = t`Save` // Recognized as Lingui
|
|
49
58
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const label = t("save") // ❌ Not confused with Lingui
|
|
59
|
+
const t = (key: string) => map[key]
|
|
60
|
+
const label = t("save") // Not confused with Lingui
|
|
53
61
|
```
|
|
54
62
|
|
|
55
|
-
##
|
|
56
|
-
|
|
57
|
-
- 🔍 Detects incorrect usage of Lingui translation macros
|
|
58
|
-
- 📝 Enforces simple, safe expressions inside translated messages
|
|
59
|
-
- 🎯 Detects missing localization of user-visible text
|
|
60
|
-
- 🧠 Zero-config recognition of technical strings via TypeScript types
|
|
61
|
-
- 🎨 Auto-ignores styling props (`*ClassName`, `*Color`, `*Style`, `*Icon`, `*Image`, `*Size`, `*Id`)
|
|
62
|
-
- 📦 Auto-ignores styling variables (`colorClasses`, `STATUS_COLORS`, `buttonStyles`, etc.)
|
|
63
|
-
- 🔧 Auto-ignores styling helper functions (`getStatusColor`, `getButtonClass`, etc.)
|
|
64
|
-
- 🔢 Auto-ignores numeric/symbolic strings without letters (`"1,00€"`, `"12:30"`)
|
|
65
|
-
- 🏷️ Branded types for custom ignore patterns (loggers, analytics, etc.)
|
|
66
|
-
- 🔒 Verifies Lingui macros actually come from `@lingui/*` packages (no false positives from similarly-named functions)
|
|
67
|
-
|
|
68
|
-
## Branded Types for Custom Ignore Patterns
|
|
69
|
-
|
|
70
|
-
For cases not covered by automatic detection (like custom loggers or analytics), this plugin exports branded types you can use to mark strings as "no translation needed":
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
import { unlocalized } from "eslint-plugin-lingui-typescript/types"
|
|
74
|
-
|
|
75
|
-
// Wrap your logger to ignore all string arguments
|
|
76
|
-
function createLogger(prefix = "[App]") {
|
|
77
|
-
return unlocalized({
|
|
78
|
-
debug: (...args: unknown[]) => console.debug(prefix, ...args),
|
|
79
|
-
info: (...args: unknown[]) => console.info(prefix, ...args),
|
|
80
|
-
warn: (...args: unknown[]) => console.warn(prefix, ...args),
|
|
81
|
-
error: (...args: unknown[]) => console.error(prefix, ...args),
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const logger = createLogger()
|
|
86
|
-
logger.info("Server started on port", 3000) // ✅ Automatically ignored
|
|
87
|
-
logger.error("Connection failed:", error) // ✅ Automatically ignored
|
|
88
|
-
```
|
|
63
|
+
## Getting started
|
|
89
64
|
|
|
90
|
-
###
|
|
65
|
+
### Requirements
|
|
91
66
|
|
|
92
|
-
|
|
93
|
-
|------|----------|
|
|
94
|
-
| `UnlocalizedFunction<T>` | Wrap functions/objects to ignore all string arguments |
|
|
95
|
-
| `unlocalized(value)` | Helper function for automatic type inference |
|
|
96
|
-
| `UnlocalizedText` | Generic technical strings |
|
|
97
|
-
| `UnlocalizedLog` | Logger message parameters (string only) |
|
|
98
|
-
| `UnlocalizedStyle` | Style values (colors, fonts, spacing) |
|
|
99
|
-
| `UnlocalizedClassName` | CSS class names |
|
|
100
|
-
| `UnlocalizedEvent` | Analytics/tracking event names |
|
|
101
|
-
| `UnlocalizedKey` | Storage keys, query keys |
|
|
102
|
-
|
|
103
|
-
See the [no-unlocalized-strings documentation](docs/rules/no-unlocalized-strings.md#branded-types) for detailed examples.
|
|
104
|
-
|
|
105
|
-
## Requirements
|
|
106
|
-
|
|
107
|
-
- Node.js ≥ 24
|
|
108
|
-
- ESLint ≥ 9
|
|
109
|
-
- TypeScript ≥ 5
|
|
67
|
+
- Node.js >= 24, ESLint >= 9, TypeScript >= 5
|
|
110
68
|
- `typescript-eslint` with type-aware linting enabled
|
|
111
69
|
|
|
112
|
-
|
|
70
|
+
### Installation
|
|
113
71
|
|
|
114
72
|
```bash
|
|
115
73
|
npm install --save-dev eslint-plugin-lingui-typescript
|
|
116
74
|
```
|
|
117
75
|
|
|
118
|
-
|
|
76
|
+
### Configuration
|
|
119
77
|
|
|
120
|
-
|
|
78
|
+
Add the recommended config to your `eslint.config.ts`:
|
|
121
79
|
|
|
122
80
|
```ts
|
|
123
81
|
import eslint from "@eslint/js"
|
|
@@ -139,7 +97,7 @@ export default [
|
|
|
139
97
|
]
|
|
140
98
|
```
|
|
141
99
|
|
|
142
|
-
Or
|
|
100
|
+
Or pick individual rules:
|
|
143
101
|
|
|
144
102
|
```ts
|
|
145
103
|
{
|
|
@@ -153,81 +111,112 @@ Or configure rules manually:
|
|
|
153
111
|
}
|
|
154
112
|
```
|
|
155
113
|
|
|
114
|
+
That's it. The plugin starts working immediately — DOM APIs, Intl methods, string literal unions, styling props, and numeric strings are all handled out of the box.
|
|
115
|
+
|
|
156
116
|
## Rules
|
|
157
117
|
|
|
158
118
|
| Rule | Description | Recommended |
|
|
159
119
|
|------|-------------|:-----------:|
|
|
160
|
-
| [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
|
|
161
|
-
| [no-single-variables-to-translate](docs/rules/no-single-variables-to-translate.md) | Disallows messages that consist only of variables without surrounding text
|
|
162
|
-
| [no-single-tag-to-translate](docs/rules/no-single-tag-to-translate.md) | Disallows `<Trans>` components that contain only a single JSX element without text.
|
|
163
|
-
| [no-nested-macros](docs/rules/no-nested-macros.md) | Prevents nesting Lingui macros inside each other
|
|
164
|
-
| [no-expression-in-message](docs/rules/no-expression-in-message.md) | Restricts embedded expressions to simple identifiers
|
|
165
|
-
| [t-call-in-function](docs/rules/t-call-in-function.md) | Ensures `t` macro calls
|
|
166
|
-
| [consistent-plural-format](docs/rules/consistent-plural-format.md) | Enforces consistent plural value format — either `#` hash syntax or `${var}` template literals
|
|
120
|
+
| [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. | ✅ |
|
|
121
|
+
| [no-single-variables-to-translate](docs/rules/no-single-variables-to-translate.md) | Disallows messages that consist only of variables without surrounding text — translators need context. | ✅ |
|
|
122
|
+
| [no-single-tag-to-translate](docs/rules/no-single-tag-to-translate.md) | Disallows `<Trans>` components that contain only a single JSX element without text. | ✅ |
|
|
123
|
+
| [no-nested-macros](docs/rules/no-nested-macros.md) | Prevents nesting Lingui macros inside each other. Nested macros create invalid message catalogs. | ✅ |
|
|
124
|
+
| [no-expression-in-message](docs/rules/no-expression-in-message.md) | Restricts embedded expressions to simple identifiers. Complex expressions must be extracted to named variables. | ✅ |
|
|
125
|
+
| [t-call-in-function](docs/rules/t-call-in-function.md) | Ensures `t` macro calls live inside functions, not at module scope where i18n isn't initialized yet. | ✅ |
|
|
126
|
+
| [consistent-plural-format](docs/rules/consistent-plural-format.md) | Enforces consistent plural value format — either `#` hash syntax or `${var}` template literals. | ✅ |
|
|
167
127
|
| [text-restrictions](docs/rules/text-restrictions.md) | Enforces project-specific text restrictions with custom patterns and messages. Requires configuration. | — |
|
|
168
128
|
|
|
129
|
+
## Branded types for edge cases
|
|
130
|
+
|
|
131
|
+
For strings that automatic detection can't cover (custom loggers, analytics events, internal keys), the plugin exports branded types:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { unlocalized } from "eslint-plugin-lingui-typescript/types"
|
|
135
|
+
|
|
136
|
+
const logger = unlocalized({
|
|
137
|
+
debug: (...args: unknown[]) => console.debug(...args),
|
|
138
|
+
info: (...args: unknown[]) => console.info(...args),
|
|
139
|
+
error: (...args: unknown[]) => console.error(...args),
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
logger.info("Server started on port", 3000) // Automatically ignored
|
|
143
|
+
logger.error("Connection failed:", error) // Automatically ignored
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Type | Use Case |
|
|
147
|
+
|------|----------|
|
|
148
|
+
| `UnlocalizedFunction<T>` | Wrap functions/objects to ignore all string arguments |
|
|
149
|
+
| `unlocalized(value)` | Helper function for automatic type inference |
|
|
150
|
+
| `UnlocalizedText` | Generic technical strings |
|
|
151
|
+
| `UnlocalizedLog` | Logger message parameters |
|
|
152
|
+
| `UnlocalizedStyle` | Style values (colors, fonts, spacing) |
|
|
153
|
+
| `UnlocalizedClassName` | CSS class names |
|
|
154
|
+
| `UnlocalizedEvent` | Analytics/tracking event names |
|
|
155
|
+
| `UnlocalizedKey` | Storage keys, query keys |
|
|
156
|
+
|
|
157
|
+
See the [no-unlocalized-strings documentation](docs/rules/no-unlocalized-strings.md#branded-types) for detailed examples.
|
|
158
|
+
|
|
169
159
|
## Migrating from eslint-plugin-lingui
|
|
170
160
|
|
|
171
|
-
This plugin is a
|
|
161
|
+
This plugin is a drop-in alternative to the official [eslint-plugin-lingui](https://github.com/lingui/eslint-plugin-lingui). Rule names are compatible, making migration straightforward.
|
|
172
162
|
|
|
173
|
-
###
|
|
163
|
+
### What changes
|
|
174
164
|
|
|
175
|
-
|
|
|
165
|
+
| | eslint-plugin-lingui | This plugin |
|
|
176
166
|
|---------|---------------------|--------------------------------|
|
|
177
|
-
| **
|
|
178
|
-
| **String literal unions** | Manual whitelist |
|
|
179
|
-
| **DOM API strings** | Manual whitelist |
|
|
180
|
-
| **Intl method arguments** | Manual whitelist |
|
|
181
|
-
| **Styling props**
|
|
182
|
-
| **Styling constants**
|
|
183
|
-
| **Numeric strings**
|
|
184
|
-
| **Custom ignore patterns** | `ignoreFunctions` only |
|
|
185
|
-
| **
|
|
186
|
-
| **ESLint
|
|
187
|
-
| **Config format** | Legacy `.eslintrc` | Flat config only |
|
|
167
|
+
| **Detection method** | Heuristics + manual whitelists | TypeScript type system |
|
|
168
|
+
| **String literal unions** | Manual whitelist | Auto-detected |
|
|
169
|
+
| **DOM API strings** | Manual whitelist | Auto-detected |
|
|
170
|
+
| **Intl method arguments** | Manual whitelist | Auto-detected |
|
|
171
|
+
| **Styling props** | Manual whitelist | Auto-detected |
|
|
172
|
+
| **Styling constants** | Manual whitelist | Auto-detected |
|
|
173
|
+
| **Numeric strings** | Manual whitelist | Auto-detected |
|
|
174
|
+
| **Custom ignore patterns** | `ignoreFunctions` only | Branded types (`unlocalized()`) |
|
|
175
|
+
| **Macro verification** | Name-based | Package-origin verification |
|
|
176
|
+
| **ESLint** | v8 legacy config | v9 flat config |
|
|
188
177
|
|
|
189
|
-
### Why
|
|
178
|
+
### Why switch?
|
|
190
179
|
|
|
191
|
-
|
|
180
|
+
**Less configuration.** TypeScript's type system handles what used to require dozens of whitelist entries.
|
|
192
181
|
|
|
193
|
-
|
|
182
|
+
**Fewer false positives.** Strings typed as literal unions are recognized as non-translatable without any setup.
|
|
194
183
|
|
|
195
|
-
|
|
184
|
+
**Modern ESLint.** Built for ESLint 9 flat config from the ground up.
|
|
196
185
|
|
|
197
|
-
### Rule
|
|
186
|
+
### Rule mapping
|
|
198
187
|
|
|
199
188
|
| eslint-plugin-lingui | eslint-plugin-lingui-typescript | Options |
|
|
200
189
|
|---------------------|--------------------------------|---------|
|
|
201
190
|
| `lingui/no-unlocalized-strings` | `lingui-ts/no-unlocalized-strings` | ⚠️ Different (see below) |
|
|
202
|
-
| `lingui/t-call-in-function` | `lingui-ts/t-call-in-function` | ✅
|
|
203
|
-
| `lingui/no-single-variables-to-translate` | `lingui-ts/no-single-variables-to-translate` | ✅
|
|
204
|
-
| `lingui/no-expression-in-message` | `lingui-ts/no-expression-in-message` | ✅
|
|
205
|
-
| `lingui/no-single-tag-to-translate` | `lingui-ts/no-single-tag-to-translate` | ✅
|
|
191
|
+
| `lingui/t-call-in-function` | `lingui-ts/t-call-in-function` | ✅ Compatible |
|
|
192
|
+
| `lingui/no-single-variables-to-translate` | `lingui-ts/no-single-variables-to-translate` | ✅ Compatible |
|
|
193
|
+
| `lingui/no-expression-in-message` | `lingui-ts/no-expression-in-message` | ✅ Compatible |
|
|
194
|
+
| `lingui/no-single-tag-to-translate` | `lingui-ts/no-single-tag-to-translate` | ✅ Compatible |
|
|
206
195
|
| `lingui/text-restrictions` | `lingui-ts/text-restrictions` | ✅ Compatible (`rules`), + `minLength` |
|
|
207
196
|
| `lingui/consistent-plural-format` | `lingui-ts/consistent-plural-format` | ✅ Compatible (`style`) |
|
|
208
197
|
| `lingui/no-trans-inside-trans` | `lingui-ts/no-nested-macros` | ✅ Extended (all macros) |
|
|
209
198
|
|
|
210
199
|
### Options Changes for `no-unlocalized-strings`
|
|
211
200
|
|
|
212
|
-
|
|
201
|
+
TypeScript types replace most manual configuration:
|
|
213
202
|
|
|
214
203
|
| Original Option | This Plugin | Notes |
|
|
215
204
|
|-----------------|-------------|-------|
|
|
216
|
-
| `useTsTypes` | — | Always enabled
|
|
205
|
+
| `useTsTypes` | — | Always enabled |
|
|
217
206
|
| `ignore` (array of regex) | `ignorePattern` (single regex) | Simplified |
|
|
218
|
-
| `ignoreFunctions` | `ignoreFunctions` |
|
|
219
|
-
| `ignoreNames` (with regex support) | `ignoreNames` |
|
|
207
|
+
| `ignoreFunctions` | `ignoreFunctions` | Simplified (Console/Error auto-detected) |
|
|
208
|
+
| `ignoreNames` (with regex support) | `ignoreNames` | Plain strings only |
|
|
220
209
|
| — | `ignoreProperties` | New: separate option for JSX attributes and object properties |
|
|
221
|
-
| `ignoreMethodsOnTypes` | — | Not needed (
|
|
210
|
+
| `ignoreMethodsOnTypes` | — | Not needed (handled by TypeScript) |
|
|
222
211
|
|
|
223
|
-
**What you can
|
|
212
|
+
**What you can drop from your config:**
|
|
224
213
|
- `useTsTypes: true` — always enabled
|
|
225
214
|
- Most `ignoreFunctions` entries for DOM APIs — auto-detected via types
|
|
226
215
|
- Most `ignoreNames` entries for typed parameters — auto-detected via types
|
|
227
216
|
- Most `ignoreProperties` entries (like `type`, `role`, `href`) — auto-detected via types
|
|
228
217
|
- `ignoreMethodsOnTypes` — handled automatically
|
|
229
218
|
|
|
230
|
-
### Migration
|
|
219
|
+
### Migration steps
|
|
231
220
|
|
|
232
221
|
1. Remove the old plugin:
|
|
233
222
|
```bash
|
|
@@ -252,17 +241,17 @@ The `no-unlocalized-strings` rule has different options because TypeScript types
|
|
|
252
241
|
|
|
253
242
|
4. Update rule names in your config (change prefix from `lingui/` to `lingui-ts/`).
|
|
254
243
|
|
|
255
|
-
5. Review your ignore lists — many entries
|
|
244
|
+
5. Review your ignore lists — many entries are no longer needed.
|
|
256
245
|
|
|
257
246
|
## Contributing
|
|
258
247
|
|
|
259
|
-
Contributions are welcome
|
|
248
|
+
Contributions are welcome. Please read our [Contributing Guide](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md) before submitting a PR.
|
|
260
249
|
|
|
261
|
-
## Related
|
|
250
|
+
## Related projects
|
|
262
251
|
|
|
263
|
-
- [Lingui](https://lingui.dev/)
|
|
264
|
-
- [eslint-plugin-lingui](https://github.com/lingui/eslint-plugin-lingui)
|
|
265
|
-
- [typescript-eslint](https://typescript-eslint.io/)
|
|
252
|
+
- [Lingui](https://lingui.dev/) — The i18n library this plugin is built for
|
|
253
|
+
- [eslint-plugin-lingui](https://github.com/lingui/eslint-plugin-lingui) — The official Lingui ESLint plugin for JavaScript projects
|
|
254
|
+
- [typescript-eslint](https://typescript-eslint.io/) — The foundation that makes type-aware linting possible
|
|
266
255
|
|
|
267
256
|
## License
|
|
268
257
|
|
|
@@ -270,4 +259,4 @@ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md)
|
|
|
270
259
|
|
|
271
260
|
---
|
|
272
261
|
|
|
273
|
-
Made with
|
|
262
|
+
Made with care by [Sebastian Software](https://www.sebastian-software.de)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAkBH,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;aAeK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CACvC,CAAA;AAqBD,eAAe,MAAM,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* @packageDocumentation
|
|
5
5
|
*/
|
|
6
6
|
import { consistentPluralFormat } from "./rules/consistent-plural-format.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { dirname, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
const packageJsonPath = resolve(dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
11
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
12
|
+
const PLUGIN_VERSION = packageJson.version ?? "0.0.0";
|
|
10
13
|
import { noExpressionInMessage } from "./rules/no-expression-in-message.js";
|
|
11
14
|
import { noNestedMacros } from "./rules/no-nested-macros.js";
|
|
12
15
|
import { noSingleTagToTranslate } from "./rules/no-single-tag-to-translate.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAA;AAC3F,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAyB,CAAA;AAC7F,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,IAAI,OAAO,CAAA;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uCAAuC,CAAA;AAC9E,OAAO,EAAE,4BAA4B,EAAE,MAAM,6CAA6C,CAAA;AAC1F,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAA;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAE/D,MAAM,MAAM,GAAG;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,iCAAiC;QACvC,OAAO,EAAE,cAAc;KACxB;IACD,KAAK,EAAE;QACL,0BAA0B,EAAE,sBAAsB;QAClD,0BAA0B,EAAE,qBAAqB;QACjD,kBAAkB,EAAE,cAAc;QAClC,4BAA4B,EAAE,sBAAsB;QACpD,kCAAkC,EAAE,4BAA4B;QAChE,wBAAwB,EAAE,oBAAoB;QAC9C,mBAAmB,EAAE,gBAAgB;QACrC,oBAAoB,EAAE,eAAe;KACtC;IACD,OAAO,EAAE,EAA6B;CACvC,CAAA;AAED,sCAAsC;AACtC,MAAM,CAAC,OAAO,GAAG;IACf,kBAAkB,EAAE;QAClB,OAAO,EAAE;YACP,WAAW,EAAE,MAAM;SACpB;QACD,KAAK,EAAE;YACL,oCAAoC,EAAE,OAAO;YAC7C,oCAAoC,EAAE,OAAO;YAC7C,4BAA4B,EAAE,OAAO;YACrC,sCAAsC,EAAE,OAAO;YAC/C,4CAA4C,EAAE,OAAO;YACrD,kCAAkC,EAAE,OAAO;YAC3C,8BAA8B,EAAE,OAAO;YACvC,gEAAgE;SACjE;KACF;CACF,CAAA;AAED,eAAe,MAAM,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-unlocalized-strings.d.ts","sourceRoot":"","sources":["../../src/rules/no-unlocalized-strings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAA;AAQrF,MAAM,WAAW,OAAO;IACtB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;
|
|
1
|
+
{"version":3,"file":"no-unlocalized-strings.d.ts","sourceRoot":"","sources":["../../src/rules/no-unlocalized-strings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAA;AAQrF,MAAM,WAAW,OAAO;IACtB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,gBAAgB,EAAE,MAAM,EAAE,CAAA;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAykDD,eAAO,MAAM,oBAAoB,2FA4M/B,CAAA"}
|
|
@@ -745,20 +745,19 @@ function isInsideStylingConstant(node) {
|
|
|
745
745
|
// ============================================================================
|
|
746
746
|
// Syntax Context Checks (non-user-facing locations)
|
|
747
747
|
// ============================================================================
|
|
748
|
-
/** React directive strings */
|
|
749
|
-
const REACT_DIRECTIVES = new Set(["use client", "use server"]);
|
|
750
748
|
/**
|
|
751
749
|
* Checks if a string literal is a React directive.
|
|
752
750
|
*
|
|
753
751
|
* React directives are special string literals:
|
|
754
752
|
* - "use client" - marks a client component boundary (file level)
|
|
755
753
|
* - "use server" - marks server actions (file level or inside async functions)
|
|
754
|
+
* - "use ..." - marks a generic directive used outside of React context, e.g. Convex (file level)
|
|
756
755
|
*/
|
|
757
756
|
function isReactDirective(node) {
|
|
758
757
|
if (node.type !== AST_NODE_TYPES.Literal || typeof node.value !== "string") {
|
|
759
758
|
return false;
|
|
760
759
|
}
|
|
761
|
-
if (!
|
|
760
|
+
if (!node.value.startsWith("use ")) {
|
|
762
761
|
return false;
|
|
763
762
|
}
|
|
764
763
|
// Must be wrapped in an expression statement
|
|
@@ -771,12 +770,13 @@ function isReactDirective(node) {
|
|
|
771
770
|
if (grandparent.type === AST_NODE_TYPES.Program) {
|
|
772
771
|
return true;
|
|
773
772
|
}
|
|
774
|
-
// Function-level directive
|
|
773
|
+
// Function-level directive: only "use server" is valid
|
|
775
774
|
if (grandparent.type === AST_NODE_TYPES.BlockStatement) {
|
|
776
775
|
const functionParent = grandparent.parent;
|
|
777
|
-
|
|
776
|
+
const isFunction = functionParent.type === AST_NODE_TYPES.FunctionDeclaration ||
|
|
778
777
|
functionParent.type === AST_NODE_TYPES.FunctionExpression ||
|
|
779
|
-
functionParent.type === AST_NODE_TYPES.ArrowFunctionExpression
|
|
778
|
+
functionParent.type === AST_NODE_TYPES.ArrowFunctionExpression;
|
|
779
|
+
return isFunction && node.value === "use server";
|
|
780
780
|
}
|
|
781
781
|
return false;
|
|
782
782
|
}
|
|
@@ -911,6 +911,201 @@ function hasLinguiIgnoreBrand(type, typeChecker) {
|
|
|
911
911
|
const typeString = typeChecker.typeToString(type);
|
|
912
912
|
return typeString.includes(LINGUI_IGNORE_BRAND);
|
|
913
913
|
}
|
|
914
|
+
function resolveSymbol(symbol, typeChecker) {
|
|
915
|
+
return (symbol.flags & ts.SymbolFlags.Alias) !== 0 ? typeChecker.getAliasedSymbol(symbol) : symbol;
|
|
916
|
+
}
|
|
917
|
+
function getEntityNameText(entityName) {
|
|
918
|
+
if (ts.isIdentifier(entityName)) {
|
|
919
|
+
return entityName.text;
|
|
920
|
+
}
|
|
921
|
+
return `${getEntityNameText(entityName.left)}.${entityName.right.text}`;
|
|
922
|
+
}
|
|
923
|
+
function getRecordKeyTypeFromTypeNode(typeNode, typeChecker, visitedSymbols) {
|
|
924
|
+
if (ts.isParenthesizedTypeNode(typeNode)) {
|
|
925
|
+
return getRecordKeyTypeFromTypeNode(typeNode.type, typeChecker, visitedSymbols);
|
|
926
|
+
}
|
|
927
|
+
if (ts.isUnionTypeNode(typeNode) || ts.isIntersectionTypeNode(typeNode)) {
|
|
928
|
+
for (const childType of typeNode.types) {
|
|
929
|
+
const keyType = getRecordKeyTypeFromTypeNode(childType, typeChecker, visitedSymbols);
|
|
930
|
+
if (keyType !== undefined) {
|
|
931
|
+
return keyType;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return undefined;
|
|
935
|
+
}
|
|
936
|
+
if (ts.isMappedTypeNode(typeNode)) {
|
|
937
|
+
const constraint = typeNode.typeParameter.constraint;
|
|
938
|
+
if (constraint !== undefined) {
|
|
939
|
+
return typeChecker.getTypeFromTypeNode(constraint);
|
|
940
|
+
}
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
if (!ts.isTypeReferenceNode(typeNode)) {
|
|
944
|
+
return undefined;
|
|
945
|
+
}
|
|
946
|
+
if (getEntityNameText(typeNode.typeName) === "Record") {
|
|
947
|
+
const keyTypeArg = typeNode.typeArguments?.[0];
|
|
948
|
+
if (keyTypeArg !== undefined) {
|
|
949
|
+
return typeChecker.getTypeFromTypeNode(keyTypeArg);
|
|
950
|
+
}
|
|
951
|
+
return undefined;
|
|
952
|
+
}
|
|
953
|
+
const symbol = typeChecker.getSymbolAtLocation(typeNode.typeName);
|
|
954
|
+
if (symbol === undefined) {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
const resolved = resolveSymbol(symbol, typeChecker);
|
|
958
|
+
if (visitedSymbols.has(resolved)) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
visitedSymbols.add(resolved);
|
|
962
|
+
const declarations = resolved.getDeclarations() ?? [];
|
|
963
|
+
for (const declaration of declarations) {
|
|
964
|
+
if (ts.isTypeAliasDeclaration(declaration)) {
|
|
965
|
+
const keyType = getRecordKeyTypeFromTypeNode(declaration.type, typeChecker, visitedSymbols);
|
|
966
|
+
if (keyType !== undefined) {
|
|
967
|
+
return keyType;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
if (ts.isInterfaceDeclaration(declaration)) {
|
|
971
|
+
for (const member of declaration.members) {
|
|
972
|
+
if (ts.isIndexSignatureDeclaration(member)) {
|
|
973
|
+
const keyParamType = member.parameters[0]?.type;
|
|
974
|
+
if (keyParamType !== undefined) {
|
|
975
|
+
return typeChecker.getTypeFromTypeNode(keyParamType);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const heritageClause of declaration.heritageClauses ?? []) {
|
|
980
|
+
for (const clauseType of heritageClause.types) {
|
|
981
|
+
const keyType = getRecordKeyTypeFromTypeNode(clauseType, typeChecker, visitedSymbols);
|
|
982
|
+
if (keyType !== undefined) {
|
|
983
|
+
return keyType;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return undefined;
|
|
990
|
+
}
|
|
991
|
+
function getRecordKeyType(type, typeChecker, visitedSymbols = new Set()) {
|
|
992
|
+
if (type.aliasSymbol !== undefined && type.aliasSymbol.escapedName.toString() === "Record") {
|
|
993
|
+
const keyType = type.aliasTypeArguments?.[0];
|
|
994
|
+
if (keyType !== undefined) {
|
|
995
|
+
return keyType;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
if (type.isUnion() || type.isIntersection()) {
|
|
999
|
+
for (const childType of type.types) {
|
|
1000
|
+
const keyType = getRecordKeyType(childType, typeChecker, visitedSymbols);
|
|
1001
|
+
if (keyType !== undefined) {
|
|
1002
|
+
return keyType;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
const symbol = type.aliasSymbol ?? type.getSymbol();
|
|
1007
|
+
if (symbol === undefined) {
|
|
1008
|
+
return undefined;
|
|
1009
|
+
}
|
|
1010
|
+
const resolved = resolveSymbol(symbol, typeChecker);
|
|
1011
|
+
if (visitedSymbols.has(resolved)) {
|
|
1012
|
+
return undefined;
|
|
1013
|
+
}
|
|
1014
|
+
visitedSymbols.add(resolved);
|
|
1015
|
+
const declarations = resolved.getDeclarations() ?? [];
|
|
1016
|
+
for (const declaration of declarations) {
|
|
1017
|
+
if (ts.isTypeAliasDeclaration(declaration)) {
|
|
1018
|
+
const keyType = getRecordKeyTypeFromTypeNode(declaration.type, typeChecker, visitedSymbols);
|
|
1019
|
+
if (keyType !== undefined) {
|
|
1020
|
+
return keyType;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (ts.isInterfaceDeclaration(declaration)) {
|
|
1024
|
+
for (const member of declaration.members) {
|
|
1025
|
+
if (ts.isIndexSignatureDeclaration(member)) {
|
|
1026
|
+
const keyParamType = member.parameters[0]?.type;
|
|
1027
|
+
if (keyParamType !== undefined) {
|
|
1028
|
+
return typeChecker.getTypeFromTypeNode(keyParamType);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return undefined;
|
|
1035
|
+
}
|
|
1036
|
+
function containsStringLikeKeyType(type, typeChecker) {
|
|
1037
|
+
if (type.isUnion() || type.isIntersection()) {
|
|
1038
|
+
return type.types.some((t) => containsStringLikeKeyType(t, typeChecker));
|
|
1039
|
+
}
|
|
1040
|
+
const flags = type.getFlags();
|
|
1041
|
+
if ((flags & ts.TypeFlags.String) !== 0 ||
|
|
1042
|
+
(flags & ts.TypeFlags.StringLiteral) !== 0 ||
|
|
1043
|
+
(flags & ts.TypeFlags.TemplateLiteral) !== 0) {
|
|
1044
|
+
return true;
|
|
1045
|
+
}
|
|
1046
|
+
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1047
|
+
const constraint = typeChecker.getBaseConstraintOfType(type);
|
|
1048
|
+
if (constraint !== undefined) {
|
|
1049
|
+
return containsStringLikeKeyType(constraint, typeChecker);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
function hasUnrestrictedStringKeyType(type, typeChecker) {
|
|
1055
|
+
if (type.isUnion()) {
|
|
1056
|
+
return type.types.some((t) => hasUnrestrictedStringKeyType(t, typeChecker));
|
|
1057
|
+
}
|
|
1058
|
+
if (type.isIntersection()) {
|
|
1059
|
+
// Branded intersections like string & { __linguiIgnore?: ... } are intentionally constrained.
|
|
1060
|
+
if (hasLinguiIgnoreBrand(type, typeChecker)) {
|
|
1061
|
+
return false;
|
|
1062
|
+
}
|
|
1063
|
+
return type.types.some((t) => hasUnrestrictedStringKeyType(t, typeChecker));
|
|
1064
|
+
}
|
|
1065
|
+
const flags = type.getFlags();
|
|
1066
|
+
if ((flags & ts.TypeFlags.String) !== 0 || (flags & ts.TypeFlags.Any) !== 0 || (flags & ts.TypeFlags.Unknown) !== 0) {
|
|
1067
|
+
return true;
|
|
1068
|
+
}
|
|
1069
|
+
if ((flags & ts.TypeFlags.TypeParameter) !== 0) {
|
|
1070
|
+
const constraint = typeChecker.getBaseConstraintOfType(type);
|
|
1071
|
+
if (constraint !== undefined) {
|
|
1072
|
+
return hasUnrestrictedStringKeyType(constraint, typeChecker);
|
|
1073
|
+
}
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
function isTechnicalObjectKeyType(type, typeChecker) {
|
|
1079
|
+
if (hasLinguiIgnoreBrand(type, typeChecker)) {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
if (!containsStringLikeKeyType(type, typeChecker)) {
|
|
1083
|
+
return false;
|
|
1084
|
+
}
|
|
1085
|
+
return !hasUnrestrictedStringKeyType(type, typeChecker);
|
|
1086
|
+
}
|
|
1087
|
+
function isTechnicalObjectKeyLiteral(node, typeChecker, parserServices) {
|
|
1088
|
+
const parent = node.parent;
|
|
1089
|
+
if (parent.type !== AST_NODE_TYPES.Property || parent.key !== node || parent.computed) {
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
const objectExpression = parent.parent;
|
|
1093
|
+
if (objectExpression.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
const objectTsNode = parserServices.esTreeNodeToTSNodeMap.get(objectExpression);
|
|
1098
|
+
const contextualType = typeChecker.getContextualType(objectTsNode);
|
|
1099
|
+
if (contextualType === undefined) {
|
|
1100
|
+
return false;
|
|
1101
|
+
}
|
|
1102
|
+
const keyType = getRecordKeyType(contextualType, typeChecker);
|
|
1103
|
+
return keyType !== undefined && isTechnicalObjectKeyType(keyType, typeChecker);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
914
1109
|
/**
|
|
915
1110
|
* Checks if a function call's callee (the function/method being called) has the
|
|
916
1111
|
* __linguiIgnoreArgs brand, meaning all string arguments should be ignored.
|
|
@@ -1204,6 +1399,11 @@ export const noUnlocalizedStrings = createRule({
|
|
|
1204
1399
|
if (ignoreRegex?.test(value) === true) {
|
|
1205
1400
|
return;
|
|
1206
1401
|
}
|
|
1402
|
+
// Object-literal key in technical Record-key context
|
|
1403
|
+
// (e.g., Record<UnlocalizedKey, ...> or Record<"First Name" | "Street", ...>)
|
|
1404
|
+
if (isTechnicalObjectKeyLiteral(node, typeChecker, parserServices)) {
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1207
1407
|
// Heuristic: does it look like user text?
|
|
1208
1408
|
if (!looksLikeUIString(value)) {
|
|
1209
1409
|
return;
|