@nkzw/eslint-plugin-fbtee 0.1.3 → 0.2.0
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 +14 -4
- package/docs/rules/no-unhelpful-desc.md +1 -1
- package/docs/rules/no-unwrapped-strings.md +55 -0
- package/lib/index.cjs +194 -39
- package/lib/index.d.cts +10 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.js +191 -36
- package/package.json +3 -3
- package/src/__tests__/no-empty-strings-test.tsx +93 -3
- package/src/__tests__/no-unhelpful-desc-test.tsx +20 -0
- package/src/__tests__/no-untranslated-strings-test.tsx +279 -0
- package/src/index.tsx +9 -0
- package/src/rules/no-empty-strings.tsx +37 -41
- package/src/rules/no-unhelpful-desc.tsx +3 -6
- package/src/rules/no-untranslated-strings.tsx +141 -0
- package/src/utils.tsx +64 -2
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
2
|
+
import rule from '../rules/no-untranslated-strings.tsx';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
parserOptions: {
|
|
7
|
+
ecmaFeatures: {
|
|
8
|
+
jsx: true,
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
ruleTester.run('no-untranslated-strings', rule, {
|
|
15
|
+
invalid: [
|
|
16
|
+
{
|
|
17
|
+
code: `
|
|
18
|
+
<span>Hello world</span>;
|
|
19
|
+
`,
|
|
20
|
+
errors: [
|
|
21
|
+
{
|
|
22
|
+
messageId: 'unwrappedString',
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: `
|
|
28
|
+
<span>{'Hello world'}</span>;
|
|
29
|
+
`,
|
|
30
|
+
errors: [
|
|
31
|
+
{
|
|
32
|
+
messageId: 'unwrappedString',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
code: `
|
|
38
|
+
<span>{'Hello world' + ''}</span>;
|
|
39
|
+
`,
|
|
40
|
+
errors: [
|
|
41
|
+
{
|
|
42
|
+
messageId: 'unwrappedString',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: `
|
|
48
|
+
<div>
|
|
49
|
+
<span>Hello world</span>
|
|
50
|
+
</div>;
|
|
51
|
+
`,
|
|
52
|
+
errors: [
|
|
53
|
+
{
|
|
54
|
+
line: 3,
|
|
55
|
+
messageId: 'unwrappedString',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: `
|
|
61
|
+
<div>
|
|
62
|
+
<div></div>
|
|
63
|
+
Hello world
|
|
64
|
+
</div>;
|
|
65
|
+
`,
|
|
66
|
+
errors: [
|
|
67
|
+
{
|
|
68
|
+
line: 4,
|
|
69
|
+
messageId: 'unwrappedString',
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: `
|
|
75
|
+
<>Hello world</>;
|
|
76
|
+
`,
|
|
77
|
+
errors: [
|
|
78
|
+
{
|
|
79
|
+
messageId: 'unwrappedString',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: `
|
|
85
|
+
<span>{foo ? 'bar' : 'baz'}</span>;
|
|
86
|
+
`,
|
|
87
|
+
errors: [
|
|
88
|
+
{
|
|
89
|
+
messageId: 'unwrappedString',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: `
|
|
95
|
+
<span>{foo ?? 'bar'}</span>;
|
|
96
|
+
`,
|
|
97
|
+
errors: [
|
|
98
|
+
{
|
|
99
|
+
messageId: 'unwrappedString',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: `
|
|
105
|
+
<span>{foo || 'bar'}</span>;
|
|
106
|
+
`,
|
|
107
|
+
errors: [
|
|
108
|
+
{
|
|
109
|
+
messageId: 'unwrappedString',
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
code: `
|
|
115
|
+
const foo = true;
|
|
116
|
+
<span>{foo && 'bar'}</span>;
|
|
117
|
+
`,
|
|
118
|
+
errors: [
|
|
119
|
+
{
|
|
120
|
+
messageId: 'unwrappedString',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
code: `
|
|
126
|
+
<input placeholder="Enter name..." />;
|
|
127
|
+
`,
|
|
128
|
+
errors: [
|
|
129
|
+
{
|
|
130
|
+
messageId: 'unwrappedString',
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
code: `
|
|
136
|
+
<div aria-label={'Label'}></div>
|
|
137
|
+
`,
|
|
138
|
+
errors: [
|
|
139
|
+
{
|
|
140
|
+
messageId: 'unwrappedString',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
code: `
|
|
146
|
+
<div aria-label={\`Label\`}></div>
|
|
147
|
+
`,
|
|
148
|
+
errors: [
|
|
149
|
+
{
|
|
150
|
+
messageId: 'unwrappedString',
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
|
|
156
|
+
valid: [
|
|
157
|
+
{
|
|
158
|
+
code: `
|
|
159
|
+
<fbt desc="Greeting">Hello world</fbt>
|
|
160
|
+
`,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
code: `
|
|
164
|
+
<fbt desc="Greeting">
|
|
165
|
+
Hello world
|
|
166
|
+
</fbt>
|
|
167
|
+
`,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
code: `
|
|
171
|
+
<fbt desc="Greeting">
|
|
172
|
+
<span>Hello world</span>
|
|
173
|
+
</fbt>
|
|
174
|
+
`,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
code: `
|
|
178
|
+
<span>
|
|
179
|
+
{fbt('Hello', 'Greeting')}
|
|
180
|
+
</span>
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
code: `
|
|
185
|
+
<fbt desc="Counter">
|
|
186
|
+
Count:
|
|
187
|
+
<fbt:plural count={1} showCount="yes" many="items">
|
|
188
|
+
item
|
|
189
|
+
</fbt:plural>
|
|
190
|
+
</fbt>
|
|
191
|
+
`,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
code: `
|
|
195
|
+
<fbt desc="Greeting">
|
|
196
|
+
<>Hello</>
|
|
197
|
+
</fbt>
|
|
198
|
+
`,
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
code: `
|
|
202
|
+
<fbt desc="Greeting">
|
|
203
|
+
Hello <span>world</span>
|
|
204
|
+
</fbt>;
|
|
205
|
+
`,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
code: `
|
|
209
|
+
<fbt desc="Welcome message">
|
|
210
|
+
Welcome <fbt:param name="name">Steve</fbt:param>
|
|
211
|
+
</fbt>
|
|
212
|
+
`,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
code: `
|
|
216
|
+
<div>
|
|
217
|
+
<p>GitHub</p>
|
|
218
|
+
<p>{'GitHub'}</p>
|
|
219
|
+
<p>{\`GitHub\`}</p>
|
|
220
|
+
</div>
|
|
221
|
+
`,
|
|
222
|
+
options: [
|
|
223
|
+
{
|
|
224
|
+
ignoredWords: ['GitHub'],
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
code: `
|
|
230
|
+
<textarea rows={2}></textarea>
|
|
231
|
+
`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
code: `
|
|
235
|
+
<a aria-current></a>
|
|
236
|
+
`,
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
code: `
|
|
240
|
+
<img src="" alt={fbs('A photo of a dog', 'Image description')} />
|
|
241
|
+
`,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
code: `
|
|
245
|
+
<div aria-label={fbs('Hello', 'Greeting')}></div>
|
|
246
|
+
`,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
code: `
|
|
250
|
+
<div aria-label={fbt('Hello', 'Greeting').toString()}></div>
|
|
251
|
+
`,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
code: `
|
|
255
|
+
foo('Bar')
|
|
256
|
+
`,
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
code: `
|
|
260
|
+
foo.bar('Baz')
|
|
261
|
+
`,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
code: `
|
|
265
|
+
const foo = isGreeting ? 'Hello' : 'Goodbye'
|
|
266
|
+
`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
code: `
|
|
270
|
+
const foo = helloGreeting ?? 'goodbye'
|
|
271
|
+
`,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
code: `
|
|
275
|
+
const foo = helloGreeting || 'goodbye'
|
|
276
|
+
`,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import packageJson from '../package.json' with { type: 'json' };
|
|
2
2
|
import noEmptyStringsRule from './rules/no-empty-strings.tsx';
|
|
3
3
|
import noUnhelpfulDesc from './rules/no-unhelpful-desc.tsx';
|
|
4
|
+
import noUntranslatedStrings from './rules/no-untranslated-strings.tsx';
|
|
4
5
|
|
|
5
6
|
export const meta = {
|
|
6
7
|
name: packageJson.name,
|
|
@@ -10,13 +11,21 @@ export const meta = {
|
|
|
10
11
|
export const rules = {
|
|
11
12
|
'no-empty-strings': noEmptyStringsRule,
|
|
12
13
|
'no-unhelpful-desc': noUnhelpfulDesc,
|
|
14
|
+
'no-untranslated-strings': noUntranslatedStrings,
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const configs = {
|
|
18
|
+
recommended: {
|
|
19
|
+
rules: {
|
|
20
|
+
'@nkzw/fbtee/no-empty-strings': 2,
|
|
21
|
+
'@nkzw/fbtee/no-unhelpful-desc': 2,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
16
24
|
strict: {
|
|
17
25
|
rules: {
|
|
18
26
|
'@nkzw/fbtee/no-empty-strings': 2,
|
|
19
27
|
'@nkzw/fbtee/no-unhelpful-desc': 2,
|
|
28
|
+
'@nkzw/fbtee/no-untranslated-strings': 2,
|
|
20
29
|
},
|
|
21
30
|
},
|
|
22
31
|
};
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createRule,
|
|
4
|
+
elementType,
|
|
5
|
+
isFbtNode,
|
|
6
|
+
resolveNodeValue,
|
|
7
|
+
} from '../utils.tsx';
|
|
3
8
|
|
|
4
9
|
export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
5
10
|
create(context) {
|
|
6
11
|
return {
|
|
7
12
|
CallExpression(node) {
|
|
8
|
-
if (
|
|
9
|
-
node.callee.type !== 'Identifier' ||
|
|
10
|
-
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
11
|
-
) {
|
|
13
|
+
if (!isFbtNode(node)) {
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -23,21 +25,22 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
23
25
|
},
|
|
24
26
|
|
|
25
27
|
JSXElement(node) {
|
|
26
|
-
if (
|
|
28
|
+
if (!isFbtNode(node)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (node.openingElement.name.type === 'JSXNamespacedName') {
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// Collect nodes to report and validate all children recursively.
|
|
31
|
-
const
|
|
32
|
-
const hasTextContent = validateChildren(node, nodesToReport);
|
|
37
|
+
const hasTextContent = validateChildren(node);
|
|
33
38
|
|
|
34
39
|
if (!hasTextContent) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
}
|
|
40
|
+
context.report({
|
|
41
|
+
messageId: 'jsxEmptyString',
|
|
42
|
+
node,
|
|
43
|
+
});
|
|
41
44
|
}
|
|
42
45
|
},
|
|
43
46
|
};
|
|
@@ -49,9 +52,9 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
49
52
|
},
|
|
50
53
|
messages: {
|
|
51
54
|
emptyString:
|
|
52
|
-
'Empty strings are not allowed in fbt()
|
|
55
|
+
'Empty strings are not allowed in fbt() and fbs() function arguments.',
|
|
53
56
|
jsxEmptyString:
|
|
54
|
-
'Empty strings are not allowed as children of
|
|
57
|
+
'Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags.',
|
|
55
58
|
},
|
|
56
59
|
schema: [],
|
|
57
60
|
type: 'problem',
|
|
@@ -61,34 +64,31 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
61
64
|
|
|
62
65
|
function validateChildren(
|
|
63
66
|
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
64
|
-
nodesToReport: Set<TSESTree.Node>,
|
|
65
67
|
): boolean {
|
|
66
68
|
let hasTextContent = false;
|
|
67
69
|
|
|
68
70
|
for (const child of node.children) {
|
|
69
|
-
if (child.type === 'JSXText' &&
|
|
70
|
-
|
|
71
|
-
nodesToReport.add(child);
|
|
72
|
-
} else {
|
|
73
|
-
hasTextContent = true;
|
|
74
|
-
}
|
|
71
|
+
if (child.type === 'JSXText' && child.value.trim() !== '') {
|
|
72
|
+
hasTextContent = true;
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
if (
|
|
78
|
-
child.type === '
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
if (child.type === 'JSXExpressionContainer') {
|
|
76
|
+
if (child.expression.type === 'JSXEmptyExpression') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Ignore when a variable is used <fbt desc="Greeting">{dynamicValue}</fbt>
|
|
81
81
|
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
node.children.every(
|
|
85
|
-
(otherChild) =>
|
|
86
|
-
child === otherChild ||
|
|
87
|
-
(otherChild.type === 'JSXText' && otherChild.value.trim() === ''),
|
|
88
|
-
))
|
|
82
|
+
child.expression.type === 'Identifier' &&
|
|
83
|
+
child.expression.name !== 'undefined'
|
|
89
84
|
) {
|
|
90
|
-
|
|
91
|
-
|
|
85
|
+
hasTextContent = true;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const value = resolveNodeValue(child.expression)?.trim();
|
|
90
|
+
|
|
91
|
+
if (value) {
|
|
92
92
|
hasTextContent = true;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -101,7 +101,7 @@ function validateChildren(
|
|
|
101
101
|
continue;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
hasTextContent ||= validateChildren(child
|
|
104
|
+
hasTextContent ||= validateChildren(child);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
if (hasTextContent) {
|
|
@@ -109,10 +109,6 @@ function validateChildren(
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (!hasTextContent && nodesToReport.size === 0) {
|
|
113
|
-
nodesToReport.add(node);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
112
|
return hasTextContent;
|
|
117
113
|
}
|
|
118
114
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRule,
|
|
3
|
-
|
|
3
|
+
isFbtNode,
|
|
4
4
|
resolveJsxElementTextContent,
|
|
5
5
|
resolveNodeValue,
|
|
6
6
|
} from '../utils.tsx';
|
|
@@ -12,10 +12,7 @@ export default createRule<
|
|
|
12
12
|
create(context) {
|
|
13
13
|
return {
|
|
14
14
|
CallExpression(node) {
|
|
15
|
-
if (
|
|
16
|
-
node.callee.type !== 'Identifier' ||
|
|
17
|
-
!(node.callee.name === 'fbt' || node.callee.name === 'fbs')
|
|
18
|
-
) {
|
|
15
|
+
if (!isFbtNode(node)) {
|
|
19
16
|
return;
|
|
20
17
|
}
|
|
21
18
|
|
|
@@ -56,7 +53,7 @@ export default createRule<
|
|
|
56
53
|
},
|
|
57
54
|
|
|
58
55
|
JSXAttribute(node) {
|
|
59
|
-
if (
|
|
56
|
+
if (!isFbtNode(node.parent.parent)) {
|
|
60
57
|
return;
|
|
61
58
|
}
|
|
62
59
|
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createRule,
|
|
3
|
+
getPropName,
|
|
4
|
+
hasFbtParent,
|
|
5
|
+
isFbtNode,
|
|
6
|
+
resolveNodeValue,
|
|
7
|
+
} from '../utils.tsx';
|
|
8
|
+
|
|
9
|
+
type Options = [{ ignoredWords: Array<string> }];
|
|
10
|
+
|
|
11
|
+
const attributes = new Set([
|
|
12
|
+
'title',
|
|
13
|
+
'placeholder',
|
|
14
|
+
'alt',
|
|
15
|
+
'label',
|
|
16
|
+
'aria-label',
|
|
17
|
+
'aria-errormessage',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export default createRule<Options, 'unwrappedString'>({
|
|
21
|
+
create(context, options) {
|
|
22
|
+
const ignoredWords = new Set(
|
|
23
|
+
options[0].ignoredWords.map((s) => s.trim().toLowerCase()),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function isIgnoredWord(value: string) {
|
|
27
|
+
return ignoredWords.has(value.trim().toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
JSXAttribute(node) {
|
|
32
|
+
const propName = getPropName(node);
|
|
33
|
+
|
|
34
|
+
if (!attributes.has(propName)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const value = node.value ? resolveNodeValue(node.value) : null;
|
|
39
|
+
|
|
40
|
+
if (!value) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isIgnoredWord(value)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
node.value?.type === 'JSXExpressionContainer' &&
|
|
50
|
+
isFbtNode(node.value.expression)
|
|
51
|
+
) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
context.report({
|
|
56
|
+
messageId: 'unwrappedString',
|
|
57
|
+
node,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
JSXExpressionContainer(node) {
|
|
62
|
+
if (node.parent.type === 'JSXAttribute') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const value = resolveNodeValue(node.expression);
|
|
67
|
+
|
|
68
|
+
if (!value) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (isIgnoredWord(value)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!hasFbtParent(node)) {
|
|
77
|
+
context.report({
|
|
78
|
+
messageId: 'unwrappedString',
|
|
79
|
+
node,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
JSXText(node) {
|
|
85
|
+
const value = node.value.trim();
|
|
86
|
+
|
|
87
|
+
if (!value) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isIgnoredWord(value)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasFbtParent(node)) {
|
|
96
|
+
const startOffset = node.range[0] + node.value.indexOf(value);
|
|
97
|
+
const endOffset = startOffset + value.length;
|
|
98
|
+
|
|
99
|
+
context.report({
|
|
100
|
+
loc: {
|
|
101
|
+
end: context.sourceCode.getLocFromIndex(endOffset),
|
|
102
|
+
start: context.sourceCode.getLocFromIndex(startOffset),
|
|
103
|
+
},
|
|
104
|
+
messageId: 'unwrappedString',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
defaultOptions: [{ ignoredWords: [] }],
|
|
112
|
+
|
|
113
|
+
meta: {
|
|
114
|
+
docs: {
|
|
115
|
+
description:
|
|
116
|
+
'Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`',
|
|
117
|
+
},
|
|
118
|
+
messages: {
|
|
119
|
+
unwrappedString:
|
|
120
|
+
'Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`',
|
|
121
|
+
},
|
|
122
|
+
schema: [
|
|
123
|
+
{
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
properties: {
|
|
126
|
+
ignoredWords: {
|
|
127
|
+
items: {
|
|
128
|
+
type: 'string',
|
|
129
|
+
},
|
|
130
|
+
type: 'array',
|
|
131
|
+
uniqueItems: true,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
type: 'object',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
type: 'problem',
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
name: 'no-untranslated-strings',
|
|
141
|
+
});
|
package/src/utils.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import type { TSESTree } from '@typescript-eslint/utils';
|
|
|
3
3
|
|
|
4
4
|
export const createRule = ESLintUtils.RuleCreator(
|
|
5
5
|
(name) =>
|
|
6
|
-
`https://github.com/nkzw-tech/fbtee/
|
|
6
|
+
`https://github.com/nkzw-tech/fbtee/blob/main/packages/eslint-plugin-fbtee/docs/rules/${name}.md`,
|
|
7
7
|
);
|
|
8
8
|
|
|
9
9
|
function resolveMemberExpressions(
|
|
@@ -47,7 +47,7 @@ export function elementType(
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function resolveJsxElementTextContent(
|
|
50
|
-
node: TSESTree.JSXElement,
|
|
50
|
+
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
51
51
|
): string {
|
|
52
52
|
return node.children
|
|
53
53
|
.map((child) => {
|
|
@@ -105,5 +105,67 @@ export function resolveNodeValue(
|
|
|
105
105
|
return resolveNodeValue(node.expression);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
if (node.type === 'ConditionalExpression') {
|
|
109
|
+
const consequent = resolveNodeValue(node.consequent);
|
|
110
|
+
const alternate = resolveNodeValue(node.alternate);
|
|
111
|
+
|
|
112
|
+
return consequent || alternate || null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (node.type === 'LogicalExpression') {
|
|
116
|
+
const left = resolveNodeValue(node.left);
|
|
117
|
+
const right = resolveNodeValue(node.right);
|
|
118
|
+
|
|
119
|
+
return left || right || null;
|
|
120
|
+
}
|
|
121
|
+
|
|
108
122
|
return null;
|
|
109
123
|
}
|
|
124
|
+
|
|
125
|
+
export function getPropName(prop: TSESTree.JSXAttribute) {
|
|
126
|
+
if (prop.name.type === 'JSXNamespacedName') {
|
|
127
|
+
return `${prop.name.namespace.name}:${prop.name.name.name}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return prop.name.name;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function hasFbtParent(node: TSESTree.Node) {
|
|
134
|
+
let current = node.parent;
|
|
135
|
+
|
|
136
|
+
while (current) {
|
|
137
|
+
if (isFbtNode(current)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
current = current.parent;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isFbtNode(node?: TSESTree.Node) {
|
|
147
|
+
if (!node) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.type === 'JSXElement') {
|
|
152
|
+
const name = elementType(node);
|
|
153
|
+
return name === 'fbt' || name === 'fbs' || name?.startsWith('fbt:');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
|
|
157
|
+
const name = node.callee.name;
|
|
158
|
+
return name === 'fbt' || name === 'fbs';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
node.type === 'MemberExpression' &&
|
|
163
|
+
node.object.type === 'CallExpression' &&
|
|
164
|
+
node.object.callee.type === 'Identifier'
|
|
165
|
+
) {
|
|
166
|
+
const name = node.object.callee.name;
|
|
167
|
+
return name === 'fbt' || name === 'fbs';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|