@nkzw/eslint-plugin-fbtee 0.0.1
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/LICENSE +21 -0
- package/README.md +223 -0
- package/docs/rules/no-empty-strings.md +29 -0
- package/docs/rules/no-unhelpful-desc.md +41 -0
- package/lib/index.cjs +313 -0
- package/lib/index.d.cts +20 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +284 -0
- package/package.json +26 -0
- package/src/__tests__/no-empty-strings-test.tsx +210 -0
- package/src/__tests__/no-unhelpful-desc-test.tsx +310 -0
- package/src/index.tsx +22 -0
- package/src/rules/no-empty-strings.tsx +106 -0
- package/src/rules/no-unhelpful-desc.tsx +122 -0
- package/src/utils.tsx +109 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Nakazawa Tech & Meta Platforms, Inc. and affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# fbtee
|
|
2
|
+
|
|
3
|
+
_**fbtee** (Far Better Translations, Extended Edition) is an internationalization framework for JavaScript & React designed to be **powerful**, **flexible**, and **intuitive**._
|
|
4
|
+
|
|
5
|
+
## Why `fbtee`?
|
|
6
|
+
|
|
7
|
+
- **Inline Translations for Better Developer Experience:** Embed translations directly into your code. No need to manage translation keys or wrap your code with `t()` functions. **fbtee** uses a compiler to extract strings from your code and prepare them for translation providers.
|
|
8
|
+
- **Proven in Production:** Built on Facebook's `fbt`, with over a decade of production usage, serving billions of users and one year of production usage in [Athena Crisis](https://athenacrisis.com).
|
|
9
|
+
- **Optimized Performance with IR:** Compiles translations into an Intermediate Representation (IR) for extracting strings, and optimizes the runtime output for performance.
|
|
10
|
+
- **Easy Setup:** Quick integration with tools like Babel and Vite means you can get started instantly.
|
|
11
|
+
|
|
12
|
+
## Status: Ready for Early Adopters
|
|
13
|
+
|
|
14
|
+
This is a fork of Facebook's original `fbt` library, which has been archived. The aim of this fork is to create the best and most modern internationalization library for JavaScript & React.
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
**fbtee** requires at least Node 22, and React 19 if you are using React.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install fbtee @nkzw/babel-fbtee
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
In your `vite.config.ts`:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import fbteePreset from '@nkzw/babel-fbtee';
|
|
28
|
+
import react from '@vitejs/plugin-react';
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
plugins: [
|
|
32
|
+
react({
|
|
33
|
+
babel: {
|
|
34
|
+
presets: [fbteePreset],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**fbtee** uses three scripts to manage translations. These scripts help automate the process of collecting, creating, and compiling translations. It is recommended to add them to your `package.json`:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"scripts": {
|
|
46
|
+
"fbtee:collect": "fbtee collect --manifest < .src_manifest.json > .source_strings.json",
|
|
47
|
+
"fbtee:manifest": "fbtee manifest --src src",
|
|
48
|
+
"fbtee:translate": "fbtee translate --translations translations/*.json --jenkins > src/translations.json"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Run these commands to set up the initial translation files:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm run fbtee:manifest && npm run fbtee:collect && npm run fbtee:translate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The files generated by these commands are auto-generated and should not be checked into version control. Add the following entries to your `.gitignore`:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
.src_manifest.json
|
|
63
|
+
.source_strings.json
|
|
64
|
+
.enum_manifest.json
|
|
65
|
+
src/translations.json
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Next, set up **fbtee** in your app's initialization code (e.g., `src/index.tsx`):
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
import { IntlVariations, setupFbtee } from 'fbtee';
|
|
72
|
+
import translations from './translations.json';
|
|
73
|
+
|
|
74
|
+
setupFbtee({
|
|
75
|
+
hooks: {
|
|
76
|
+
getViewerContext: () => ({
|
|
77
|
+
GENDER: IntlVariations.GENDER_UNKNOWN,
|
|
78
|
+
locale: 'en_US',
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
translations,
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Finally, if you are using React and TypeScript in your project, you need to add TypeScript types for **fbtee** to enable proper type checking in JSX. You can do this by referencing the `ReactTypes.d.ts` file in your main `index.tsx` file or a global type declaration file (e.g., `types.d.ts`):
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
/// <reference types="fbtee/ReactTypes.d.ts" />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
_You’re now ready to go!_
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
_If you want to learn by example, check out the [examples](https://github.com/nkzw-tech/fbtee/tree/main/example) directory._
|
|
96
|
+
|
|
97
|
+
All strings need to be wrapped by `<fbt>` (for React/JSX) or `fbt()` (for JavaScript). This ensures strings can be extracted and translated properly. The `desc` attribute is required and provides context for translators, helping them understand the intended meaning of the string.
|
|
98
|
+
|
|
99
|
+
Here are some basic examples:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
const Greeting = () => <div>Hello, World!</div>;
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
You can wrap the string with `<fbt>`:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
const Greeting = () => (
|
|
109
|
+
<div>
|
|
110
|
+
<fbt desc="Greeting">Hello, World!</fbt>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`<fbt>` is a special React component that marks text for translation. The `fbtee` compiler analyzes them to extract strings, and compiles them into an Intermediate Representation (IR). It supports dynamic content through <fbt:param> or even other React components. For example, if you want to greet a specific user based on a name passed as a prop, you can use `<fbt:param>`:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
const Greeting = ({ name }) => (
|
|
119
|
+
<div>
|
|
120
|
+
<fbt desc="Greeting">
|
|
121
|
+
Hello, <fbt:param name="name">{name}</fbt:param>!
|
|
122
|
+
</fbt>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**fbtee** allows you to use regular React Components inside of `<fbt>` which will automatically create `<fbt:param>` calls for you:
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
const Greeting = ({ name }) => (
|
|
131
|
+
<div>
|
|
132
|
+
<fbt desc="Greeting">
|
|
133
|
+
Hello, <Name name={name} />!
|
|
134
|
+
</fbt>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
_Note: `<fbt>` is auto-imported for you by the `@nkzw/babel-fbtee` plugin._
|
|
140
|
+
|
|
141
|
+
After marking your strings for translation with `<fbt>`, run the following commands to extract, and compile translations:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npm run fbtee:collect
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
You can now upload the `.source_strings.json` file to your translation provider. Once you have the translated strings stored in a `translations/` folder as JSON files, you can run the following command to generate the translations file:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npm run fbtee:translate
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
After generating the translations file, your app is ready to display translated content in other languages.
|
|
154
|
+
|
|
155
|
+
## ESLint Plugin
|
|
156
|
+
|
|
157
|
+
You can install the optional eslint plugin to catch common mistakes and enforce best practices when using **fbtee**:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm install @nkzw/eslint-plugin-fbtee
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Add the following configuration to your ESLint configuration:
|
|
164
|
+
|
|
165
|
+
```js
|
|
166
|
+
{
|
|
167
|
+
extends: ['plugin:@nkzw/eslint-plugin-fbtee/strict'],
|
|
168
|
+
plugins: ['@nkzw/eslint-plugin-fbtee'],
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Or, if you'd like more granular control over the rules:
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
{
|
|
176
|
+
plugins: ['@nkzw/eslint-plugin-fbtee'],
|
|
177
|
+
rules: {
|
|
178
|
+
'@nkzw/fbtee/no-empty-strings': 'error',
|
|
179
|
+
'@nkzw/fbtee/no-unhelpful-desc': 'error',
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## What's better about `fbtee` than `fbt`?
|
|
185
|
+
|
|
186
|
+
Facebook has done an amazing job with `fbt`, an internationalization library that has been successfully used in production at Facebook for over 10 years. Their work provided a strong foundation for modern localization tools.
|
|
187
|
+
|
|
188
|
+
The open-source version of `fbt`, however, became unmaintained, difficult to set up, and incompatible with modern tools. It was eventually archived in November 2024. **fbtee** builds on this foundation with several improvements:
|
|
189
|
+
|
|
190
|
+
- **Easier Setup:** fbtee works with modern tools like Vite.
|
|
191
|
+
- **Statically Typed:** The fbtee compiler ensures correct usage of fbtee, libary TypeScript types are provided, and an eslint plugin helps fix common mistakes.
|
|
192
|
+
- **Improved React Compatibility:** Removed React-specific hacks and added support for implicit React fragments (`<>`).
|
|
193
|
+
- **Enhanced Features:** Fixed and exported `intlList`, which was not functional in the original `fbt`.
|
|
194
|
+
- **Modernized Codebase:** Rewritten using TypeScript, ES modules (ESM), eslint, and modern JavaScript standards. Removed cruft and legacy code.
|
|
195
|
+
- **Updated Tooling:** Uses modern tools like pnpm, Vite, and esbuild for faster and more efficient development of **fbtee**.
|
|
196
|
+
|
|
197
|
+
**fbtee** remains compatible with `fbt` and migration is straightforward.
|
|
198
|
+
|
|
199
|
+
## Migration from `fbt`:
|
|
200
|
+
|
|
201
|
+
**fbtee** is compatible with `fbt`. If you are already using `fbt`, migrating to fbtee is straightforward:
|
|
202
|
+
|
|
203
|
+
- Follow the "Getting Started" guide above and remove all "fbt" related packages.
|
|
204
|
+
- Make sure you are using React 19.
|
|
205
|
+
- Replace `import { fbt } from 'fbt'` with `import { fbt } from 'fbtee'`.
|
|
206
|
+
- Rename commands from `fbt-collect`, `fbt-manifest` and `fbt-translate` to `fbtee-collect`, `fbtee-manifest` and `fbtee-translate`.
|
|
207
|
+
- If you were using CommonJS modules for common strings or enums, convert them to ES modules.
|
|
208
|
+
- Ensure you are using the latest version of Node.js 22 or later.
|
|
209
|
+
- Rename your `init({})` call to `setupFbtee({})`.
|
|
210
|
+
|
|
211
|
+
After these changes, your project should work seamlessly with **fbtee**.
|
|
212
|
+
|
|
213
|
+
_Note: Some legacy behavior and options were removed from `fbtee`. If you have a complex setup, please consider [reaching out to us for help](mailto:fbtee@nakazawa.dev)._
|
|
214
|
+
|
|
215
|
+
## Credits
|
|
216
|
+
|
|
217
|
+
- `fbt` was originally created by [Facebook](https://github.com/facebook/fbt).
|
|
218
|
+
- The auto-import plugin was created by [@alexandernanberg](https://github.com/alexandernanberg).
|
|
219
|
+
- [Nakazawa Tech](https://nkzw.tech) rewrote `fbt` into `fbtee` and continues to maintain this project.
|
|
220
|
+
|
|
221
|
+
## Support
|
|
222
|
+
|
|
223
|
+
- Check out the [#fbtee channel on Reactiflux's Discord server](https://discord.gg/reactiflux).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Prevent empty strings from being given to `<fbt>` or `fbt()` (fbtee/no-empty-strings)
|
|
2
|
+
|
|
3
|
+
## Rule Details
|
|
4
|
+
|
|
5
|
+
This rule ensures that `<fbt>` tags and `fbt()`/`fbs()` calls are given meaningful text.
|
|
6
|
+
|
|
7
|
+
Examples of **incorrect** code for this rule:
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
<fbt desc="Greeting"></fbt>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<fbt desc="Greeting">{''}</fbt>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
fbt('', 'Greeting');
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Examples of **correct** code for this rule:
|
|
22
|
+
|
|
23
|
+
```jsx
|
|
24
|
+
<fbt desc="Greeting">Hello</fbt>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
fbt('Hello', 'Greeting');
|
|
29
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Prevent unhelpful descriptions being given to `<fbt>` or `fbt()` (fbtee/no-unhelpful-desc)
|
|
2
|
+
|
|
3
|
+
## Rule Details
|
|
4
|
+
|
|
5
|
+
This rule enforces that meaningful and non-empty descriptions are provided to <fbt> elements and fbt()/fbs() function calls. Descriptions (desc) are crucial for ensuring proper translation context and avoiding ambiguous or unhelpful translations.
|
|
6
|
+
|
|
7
|
+
Examples of **incorrect** code for this rule:
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
<fbt desc="">Hello world</fbt>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
<fbt desc="foo">Hello world</fbt>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```jsx
|
|
18
|
+
<fbt desc="Hello world">Hello world</fbt>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```jsx
|
|
22
|
+
fbt('Hello world', '');
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```jsx
|
|
26
|
+
fbt('Hello world', 'foo');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```jsx
|
|
30
|
+
fbt('Hello world', 'hello world');
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Examples of **correct** code for this rule:
|
|
34
|
+
|
|
35
|
+
```jsx
|
|
36
|
+
<fbt desc="Greeting">Hello</fbt>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```jsx
|
|
40
|
+
fbt('Hello', 'Greeting');
|
|
41
|
+
```
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
configs: () => configs,
|
|
24
|
+
meta: () => meta,
|
|
25
|
+
rules: () => rules
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
|
|
29
|
+
// package.json
|
|
30
|
+
var package_default = {
|
|
31
|
+
name: "@nkzw/eslint-plugin-fbtee",
|
|
32
|
+
version: "0.0.1",
|
|
33
|
+
description: "",
|
|
34
|
+
keywords: [],
|
|
35
|
+
repository: {
|
|
36
|
+
type: "git",
|
|
37
|
+
url: "git+https://github.com/nkzw-tech/fbtee.git",
|
|
38
|
+
directory: "packages/eslint-plugin-fbtee"
|
|
39
|
+
},
|
|
40
|
+
license: "MIT",
|
|
41
|
+
type: "module",
|
|
42
|
+
exports: {
|
|
43
|
+
import: "./lib/index.js",
|
|
44
|
+
require: "./lib/index.cjs"
|
|
45
|
+
},
|
|
46
|
+
scripts: {
|
|
47
|
+
build: "tsup src/index.tsx -d lib --target=node22 --format=esm,cjs --clean --no-splitting --dts"
|
|
48
|
+
},
|
|
49
|
+
dependencies: {
|
|
50
|
+
"@typescript-eslint/utils": "^8.18.0"
|
|
51
|
+
},
|
|
52
|
+
devDependencies: {
|
|
53
|
+
"@typescript-eslint/rule-tester": "^8.18.0"
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/utils.tsx
|
|
58
|
+
var import_utils = require("@typescript-eslint/utils");
|
|
59
|
+
var createRule = import_utils.ESLintUtils.RuleCreator(
|
|
60
|
+
(name) => `https://github.com/nkzw-tech/fbtee/tree/main/packages/eslint-plugin-fbt/docs/rules/${name}`
|
|
61
|
+
);
|
|
62
|
+
function resolveMemberExpressions(object, property) {
|
|
63
|
+
if (object.type === "JSXMemberExpression") {
|
|
64
|
+
return `${resolveMemberExpressions(object.object, object.property)}.${property.name}`;
|
|
65
|
+
}
|
|
66
|
+
return `${object.name}.${property.name}`;
|
|
67
|
+
}
|
|
68
|
+
function elementType(node) {
|
|
69
|
+
if (node.type === "JSXFragment") {
|
|
70
|
+
return "<>";
|
|
71
|
+
}
|
|
72
|
+
const { name } = node.openingElement;
|
|
73
|
+
switch (name.type) {
|
|
74
|
+
case "JSXIdentifier": {
|
|
75
|
+
return name.name;
|
|
76
|
+
}
|
|
77
|
+
case "JSXMemberExpression": {
|
|
78
|
+
const { object, property } = name;
|
|
79
|
+
return resolveMemberExpressions(object, property);
|
|
80
|
+
}
|
|
81
|
+
case "JSXNamespacedName": {
|
|
82
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
83
|
+
}
|
|
84
|
+
default: {
|
|
85
|
+
throw new Error("Unsupported JSX node type.");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function resolveJsxElementTextContent(node) {
|
|
90
|
+
return node.children.map((child) => {
|
|
91
|
+
if (child.type === "JSXText") {
|
|
92
|
+
return child.value.trim();
|
|
93
|
+
}
|
|
94
|
+
if (child.type === "JSXExpressionContainer") {
|
|
95
|
+
return resolveNodeValue(child.expression) || "";
|
|
96
|
+
}
|
|
97
|
+
return "";
|
|
98
|
+
}).join("").trim();
|
|
99
|
+
}
|
|
100
|
+
function resolveBinaryExpression(node) {
|
|
101
|
+
if (node.operator !== "+") {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
const leftValue = resolveNodeValue(node.left);
|
|
105
|
+
const rightValue = resolveNodeValue(node.right);
|
|
106
|
+
if (leftValue !== null && rightValue !== null) {
|
|
107
|
+
return leftValue + rightValue;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
function resolveNodeValue(node) {
|
|
112
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
113
|
+
return node.value;
|
|
114
|
+
}
|
|
115
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
116
|
+
return node.quasis.map((quasi) => quasi.value.raw).join("");
|
|
117
|
+
}
|
|
118
|
+
if (node.type === "BinaryExpression") {
|
|
119
|
+
return resolveBinaryExpression(node);
|
|
120
|
+
}
|
|
121
|
+
if (node.type === "JSXExpressionContainer") {
|
|
122
|
+
return resolveNodeValue(node.expression);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/rules/no-empty-strings.tsx
|
|
128
|
+
var no_empty_strings_default = createRule({
|
|
129
|
+
create(context) {
|
|
130
|
+
return {
|
|
131
|
+
CallExpression(node) {
|
|
132
|
+
if (node.callee.type === "Identifier" && !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const [text] = node.arguments;
|
|
136
|
+
if (text && text.type !== "SpreadElement" && isEmptyString(text)) {
|
|
137
|
+
context.report({
|
|
138
|
+
messageId: "emptyString",
|
|
139
|
+
node: text
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
JSXElement(node) {
|
|
144
|
+
if (elementType(node) !== "fbt") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const nodesToReport = /* @__PURE__ */ new Set();
|
|
148
|
+
const hasTextContent = validateChildren(node, nodesToReport);
|
|
149
|
+
if (!hasTextContent) {
|
|
150
|
+
for (const node2 of nodesToReport) {
|
|
151
|
+
context.report({
|
|
152
|
+
messageId: "jsxEmptyString",
|
|
153
|
+
node: node2
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
defaultOptions: [],
|
|
161
|
+
meta: {
|
|
162
|
+
docs: {
|
|
163
|
+
description: "Disallow empty strings in fbt elements or function calls."
|
|
164
|
+
},
|
|
165
|
+
messages: {
|
|
166
|
+
emptyString: "Empty strings are not allowed in fbt() or fbs() function arguments.",
|
|
167
|
+
jsxEmptyString: "Empty strings are not allowed as children of <fbt> tags."
|
|
168
|
+
},
|
|
169
|
+
schema: [],
|
|
170
|
+
type: "problem"
|
|
171
|
+
},
|
|
172
|
+
name: "no-empty-strings"
|
|
173
|
+
});
|
|
174
|
+
function validateChildren(node, nodesToReport) {
|
|
175
|
+
let hasTextContent = false;
|
|
176
|
+
for (const child of node.children) {
|
|
177
|
+
if (child.type === "JSXText" && node.children.length === 1) {
|
|
178
|
+
if (child.value.trim() === "") {
|
|
179
|
+
nodesToReport.add(child);
|
|
180
|
+
} else {
|
|
181
|
+
hasTextContent = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (child.type === "JSXExpressionContainer" && child.expression.type !== "JSXEmptyExpression") {
|
|
185
|
+
if (isEmptyString(child.expression)) {
|
|
186
|
+
nodesToReport.add(child.expression);
|
|
187
|
+
} else {
|
|
188
|
+
hasTextContent = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") {
|
|
192
|
+
hasTextContent ||= validateChildren(child, nodesToReport);
|
|
193
|
+
}
|
|
194
|
+
if (hasTextContent) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!hasTextContent && nodesToReport.size === 0) {
|
|
199
|
+
nodesToReport.add(node);
|
|
200
|
+
}
|
|
201
|
+
return hasTextContent;
|
|
202
|
+
}
|
|
203
|
+
function isEmptyString(node) {
|
|
204
|
+
return resolveNodeValue(node)?.trim() === "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/rules/no-unhelpful-desc.tsx
|
|
208
|
+
var no_unhelpful_desc_default = createRule({
|
|
209
|
+
create(context) {
|
|
210
|
+
return {
|
|
211
|
+
CallExpression(node) {
|
|
212
|
+
if (node.callee.type !== "Identifier" || !(node.callee.name === "fbt" || node.callee.name === "fbs")) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const [textArg, descArg] = node.arguments;
|
|
216
|
+
const desc = descArg && descArg.type !== "SpreadElement" ? resolveNodeValue(descArg)?.trim() : null;
|
|
217
|
+
if (!desc) {
|
|
218
|
+
context.report({
|
|
219
|
+
messageId: "emptyDesc",
|
|
220
|
+
node: descArg || node
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (desc.length <= 4) {
|
|
225
|
+
context.report({
|
|
226
|
+
messageId: "shortDesc",
|
|
227
|
+
node: descArg || node
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const text = textArg && textArg.type !== "SpreadElement" ? resolveNodeValue(textArg)?.trim() : null;
|
|
232
|
+
if (desc.toLowerCase() === text?.toLowerCase()) {
|
|
233
|
+
context.report({
|
|
234
|
+
messageId: "duplicateDesc",
|
|
235
|
+
node: descArg || node
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
JSXAttribute(node) {
|
|
240
|
+
if (elementType(node.parent.parent) !== "fbt") {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (node.name.name !== "desc" || !node.value) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (node.value.type === "JSXExpressionContainer" && (node.value.expression.type === "Identifier" || node.value.expression.type === "CallExpression")) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const desc = resolveNodeValue(node.value)?.trim();
|
|
250
|
+
if (!desc) {
|
|
251
|
+
context.report({
|
|
252
|
+
messageId: "jsxEmptyDesc",
|
|
253
|
+
node: node.value
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (desc.length <= 4) {
|
|
258
|
+
context.report({
|
|
259
|
+
messageId: "shortDesc",
|
|
260
|
+
node: node.value
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const textContent = resolveJsxElementTextContent(node.parent.parent);
|
|
265
|
+
if (desc.toLowerCase() === textContent.toLowerCase()) {
|
|
266
|
+
context.report({
|
|
267
|
+
messageId: "duplicateDesc",
|
|
268
|
+
node: node.value
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
defaultOptions: [],
|
|
275
|
+
meta: {
|
|
276
|
+
docs: {
|
|
277
|
+
description: "Enforce meaningful and non-empty descriptions in <fbt> elements and fbt()/fbs() function calls."
|
|
278
|
+
},
|
|
279
|
+
messages: {
|
|
280
|
+
duplicateDesc: 'The "desc" attribute or argument should not be a duplicate of the text content.',
|
|
281
|
+
emptyDesc: 'The "desc" argument in fbt() and fbs() must not be empty.',
|
|
282
|
+
jsxEmptyDesc: 'The "desc" attribute in <fbt> must not be empty.',
|
|
283
|
+
shortDesc: 'The "desc" attribute or argument is too short. Use a more descriptive and meaningful explanation.'
|
|
284
|
+
},
|
|
285
|
+
schema: [],
|
|
286
|
+
type: "problem"
|
|
287
|
+
},
|
|
288
|
+
name: "no-unhelpful-desc"
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// src/index.tsx
|
|
292
|
+
var meta = {
|
|
293
|
+
name: package_default.name,
|
|
294
|
+
version: package_default.version
|
|
295
|
+
};
|
|
296
|
+
var rules = {
|
|
297
|
+
"no-empty-strings": no_empty_strings_default,
|
|
298
|
+
"no-unhelpful-desc": no_unhelpful_desc_default
|
|
299
|
+
};
|
|
300
|
+
var configs = {
|
|
301
|
+
strict: {
|
|
302
|
+
rules: {
|
|
303
|
+
"@nkzw/fbtee/no-empty-strings": 2,
|
|
304
|
+
"@nkzw/fbtee/no-unhelpful-desc": 2
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
309
|
+
0 && (module.exports = {
|
|
310
|
+
configs,
|
|
311
|
+
meta,
|
|
312
|
+
rules
|
|
313
|
+
});
|
package/lib/index.d.cts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
|
|
3
|
+
declare const meta: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
declare const rules: {
|
|
8
|
+
'no-empty-strings': _typescript_eslint_utils_ts_eslint.RuleModule<"emptyString" | "jsxEmptyString", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
'no-unhelpful-desc': _typescript_eslint_utils_ts_eslint.RuleModule<"emptyDesc" | "jsxEmptyDesc" | "duplicateDesc" | "shortDesc", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
+
};
|
|
11
|
+
declare const configs: {
|
|
12
|
+
strict: {
|
|
13
|
+
rules: {
|
|
14
|
+
'@nkzw/fbtee/no-empty-strings': number;
|
|
15
|
+
'@nkzw/fbtee/no-unhelpful-desc': number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { configs, meta, rules };
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
|
|
3
|
+
declare const meta: {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
};
|
|
7
|
+
declare const rules: {
|
|
8
|
+
'no-empty-strings': _typescript_eslint_utils_ts_eslint.RuleModule<"emptyString" | "jsxEmptyString", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
9
|
+
'no-unhelpful-desc': _typescript_eslint_utils_ts_eslint.RuleModule<"emptyDesc" | "jsxEmptyDesc" | "duplicateDesc" | "shortDesc", [], unknown, _typescript_eslint_utils_ts_eslint.RuleListener>;
|
|
10
|
+
};
|
|
11
|
+
declare const configs: {
|
|
12
|
+
strict: {
|
|
13
|
+
rules: {
|
|
14
|
+
'@nkzw/fbtee/no-empty-strings': number;
|
|
15
|
+
'@nkzw/fbtee/no-unhelpful-desc': number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { configs, meta, rules };
|