@nkzw/eslint-plugin-fbtee 3.1.0 → 3.3.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 +39 -41
- package/lib/index.mjs +1 -1
- package/package.json +3 -3
- package/src/rules/no-empty-strings.tsx +5 -17
- package/src/rules/no-unhelpful-desc.tsx +3 -10
- package/src/rules/no-untranslated-strings.tsx +6 -20
- package/src/utils.tsx +2 -6
package/README.md
CHANGED
|
@@ -10,14 +10,14 @@ const Greeting = ({ name }) => (
|
|
|
10
10
|
);
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
_fbtee_ is a modern continuation of Facebook's `fbt`, rebuilt for TypeScript, ESM, React 19, Vite,
|
|
13
|
+
_fbtee_ is a modern continuation of Facebook's `fbt`, rebuilt for TypeScript, ESM, React 19, Vite, Next.js, Babel & Oxc.
|
|
14
14
|
|
|
15
15
|
## Features
|
|
16
16
|
|
|
17
17
|
- **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.
|
|
18
18
|
- **Proven in Production:** Built on Facebook's `fbt`, with over a decade of production usage serving billions of users, plus years in production at [Athena Crisis](https://athenacrisis.com).
|
|
19
19
|
- **Optimized Performance with IR:** Compiles translations into an Intermediate Representation (IR) for extracting strings, then optimizes the runtime output for performance.
|
|
20
|
-
- **Easy Setup:** Quick integration with Vite,
|
|
20
|
+
- **Easy Setup:** Quick integration with Vite, Next.js, Babel, Oxc, and Expo.
|
|
21
21
|
|
|
22
22
|
## Getting Started
|
|
23
23
|
|
|
@@ -62,49 +62,44 @@ export default defineConfig({
|
|
|
62
62
|
});
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
###
|
|
65
|
+
### Next.js
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
Next.js projects should use the native Oxc plugin and the CLI:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
npm install -D @nkzw/
|
|
70
|
+
npm install -D @nkzw/next-plugin-fbtee @nkzw/fbtee-cli
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Wrap your Next configuration:
|
|
74
74
|
|
|
75
75
|
```js
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
import withFbtee from '@nkzw/next-plugin-fbtee';
|
|
77
|
+
|
|
78
|
+
export default withFbtee({
|
|
79
|
+
fbtCommon: {
|
|
80
|
+
Accept: 'Button label for accepting terms',
|
|
81
|
+
},
|
|
82
|
+
fbtEnumManifest: {},
|
|
83
|
+
})({
|
|
84
|
+
// Your Next.js configuration.
|
|
85
|
+
});
|
|
79
86
|
```
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
The plugin runs the native Oxc transform before Next.js compilation. It works with the default Turbopack compiler and with `next build --webpack`, so it does not require Babel or custom compiler configuration.
|
|
89
|
+
|
|
90
|
+
### Babel
|
|
82
91
|
|
|
83
|
-
Install the
|
|
92
|
+
Install the Babel preset and the CLI:
|
|
84
93
|
|
|
85
94
|
```bash
|
|
86
|
-
npm install -D @nkzw/
|
|
95
|
+
npm install -D @nkzw/babel-preset-fbtee @nkzw/fbtee-cli
|
|
87
96
|
```
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
Add the preset to your Babel configuration:
|
|
90
99
|
|
|
91
100
|
```js
|
|
92
|
-
import { createFbteePluginOptions } from '@nkzw/swc-plugin-fbtee/index.js';
|
|
93
|
-
|
|
94
101
|
export default {
|
|
95
|
-
|
|
96
|
-
swcPlugins: [
|
|
97
|
-
[
|
|
98
|
-
'@nkzw/swc-plugin-fbtee',
|
|
99
|
-
createFbteePluginOptions({
|
|
100
|
-
fbtCommon: {
|
|
101
|
-
Accept: 'Button label for accepting terms',
|
|
102
|
-
},
|
|
103
|
-
fbtEnumManifest: {},
|
|
104
|
-
}),
|
|
105
|
-
],
|
|
106
|
-
],
|
|
107
|
-
},
|
|
102
|
+
presets: ['@nkzw/babel-preset-fbtee'],
|
|
108
103
|
};
|
|
109
104
|
```
|
|
110
105
|
|
|
@@ -159,10 +154,7 @@ Use `fbt()` outside JSX:
|
|
|
159
154
|
```tsx
|
|
160
155
|
import { fbt } from 'fbtee';
|
|
161
156
|
|
|
162
|
-
const title = fbt(
|
|
163
|
-
'No tasks yet',
|
|
164
|
-
'Empty state title when a project has no tasks',
|
|
165
|
-
);
|
|
157
|
+
const title = fbt('No tasks yet', 'Empty state title when a project has no tasks');
|
|
166
158
|
```
|
|
167
159
|
|
|
168
160
|
Use `fbs()` when you need a plain string:
|
|
@@ -170,9 +162,7 @@ Use `fbs()` when you need a plain string:
|
|
|
170
162
|
```tsx
|
|
171
163
|
import { fbs } from 'fbtee';
|
|
172
164
|
|
|
173
|
-
<input
|
|
174
|
-
placeholder={fbs('Search projects', 'Project search input placeholder')}
|
|
175
|
-
/>;
|
|
165
|
+
<input placeholder={fbs('Search projects', 'Project search input placeholder')} />;
|
|
176
166
|
```
|
|
177
167
|
|
|
178
168
|
## Dynamic Content
|
|
@@ -202,12 +192,7 @@ Use `<fbt:plural>` when a count controls grammar. _fbtee_ handles locale-specifi
|
|
|
202
192
|
```tsx
|
|
203
193
|
<fbt desc="Inbox unread count">
|
|
204
194
|
You have{' '}
|
|
205
|
-
<fbt:plural
|
|
206
|
-
count={count}
|
|
207
|
-
many="unread messages"
|
|
208
|
-
name="unreadCount"
|
|
209
|
-
showCount="yes"
|
|
210
|
-
>
|
|
195
|
+
<fbt:plural count={count} many="unread messages" name="unreadCount" showCount="yes">
|
|
211
196
|
an unread message
|
|
212
197
|
</fbt:plural>
|
|
213
198
|
.
|
|
@@ -340,6 +325,19 @@ See the [Next.js fbtee example](https://github.com/cpojer/nextjs-fbtee-example)
|
|
|
340
325
|
|
|
341
326
|
## Translation Workflow
|
|
342
327
|
|
|
328
|
+
Every CLI command can use the native Oxc/Rust pipeline by adding `--oxc`:
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
pnpm fbtee collect --oxc
|
|
332
|
+
pnpm fbtee prepare-translations --oxc --source-strings source_strings.json --output-dir translations
|
|
333
|
+
pnpm fbtee translate --oxc --source-strings source_strings.json --translations 'translations/*.json' --output-dir src/translations
|
|
334
|
+
pnpm fbtee migrate-locales --oxc --to bcp47 --dir translations --dir src/translations
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
The Babel pipeline remains the default during the parity rollout, so removing `--oxc` makes it easy to compare both implementations. Successful native outputs are tested byte-for-byte against Babel. The native collector intentionally cannot execute Babel-specific JavaScript extensions: `--custom-collector`, `--transform`, and `--generate-fbt-nodes` are unavailable with `--oxc`.
|
|
338
|
+
|
|
339
|
+
The Oxc collector also cannot run plugins or presets from a Babel configuration file or apply `.babelignore`. If it detects Babel configuration or ignore rules, it stops instead of silently collecting different source. Use the Babel collector when that preprocessing is required, or pass `--disable-babel-config` to explicitly collect the unmodified source with Oxc. All other documented options are supported.
|
|
340
|
+
|
|
343
341
|
Extract source strings:
|
|
344
342
|
|
|
345
343
|
```bash
|
package/lib/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
2
|
//#region package.json
|
|
3
3
|
var name = "@nkzw/eslint-plugin-fbtee";
|
|
4
|
-
var version = "3.
|
|
4
|
+
var version = "3.3.0";
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/utils.tsx
|
|
7
7
|
const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/nkzw-tech/fbtee/blob/main/packages/eslint-plugin-fbtee/docs/rules/${name}.md`);
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nkzw/eslint-plugin-fbtee",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [],
|
|
6
|
+
"license": "MIT",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
9
|
"url": "git+https://github.com/nkzw-tech/fbtee.git",
|
|
9
10
|
"directory": "packages/eslint-plugin-fbtee"
|
|
10
11
|
},
|
|
11
|
-
"license": "MIT",
|
|
12
12
|
"type": "module",
|
|
13
|
+
"main": "lib/index.mjs",
|
|
13
14
|
"exports": {
|
|
14
15
|
"import": "./lib/index.mjs"
|
|
15
16
|
},
|
|
16
|
-
"main": "lib/index.mjs",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@typescript-eslint/utils": "^8.64.0"
|
|
19
19
|
},
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import type { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
3
|
-
createRule,
|
|
4
|
-
elementType,
|
|
5
|
-
isFbtNode,
|
|
6
|
-
resolveNodeValue,
|
|
7
|
-
} from '../utils.tsx';
|
|
2
|
+
import { createRule, elementType, isFbtNode, resolveNodeValue } from '../utils.tsx';
|
|
8
3
|
|
|
9
4
|
export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
10
5
|
create(context) {
|
|
@@ -51,10 +46,8 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
51
46
|
description: 'Disallow empty strings in fbt elements or function calls.',
|
|
52
47
|
},
|
|
53
48
|
messages: {
|
|
54
|
-
emptyString:
|
|
55
|
-
|
|
56
|
-
jsxEmptyString:
|
|
57
|
-
'Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags.',
|
|
49
|
+
emptyString: 'Empty strings are not allowed in fbt() and fbs() function arguments.',
|
|
50
|
+
jsxEmptyString: 'Empty strings are not allowed as children of `<fbt>` and `<fbs>` tags.',
|
|
58
51
|
},
|
|
59
52
|
schema: [],
|
|
60
53
|
type: 'problem',
|
|
@@ -62,9 +55,7 @@ export default createRule<[], 'emptyString' | 'jsxEmptyString'>({
|
|
|
62
55
|
name: 'no-empty-strings',
|
|
63
56
|
});
|
|
64
57
|
|
|
65
|
-
function validateChildren(
|
|
66
|
-
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
67
|
-
): boolean {
|
|
58
|
+
function validateChildren(node: TSESTree.JSXElement | TSESTree.JSXFragment): boolean {
|
|
68
59
|
let hasTextContent = false;
|
|
69
60
|
|
|
70
61
|
for (const child of node.children) {
|
|
@@ -78,10 +69,7 @@ function validateChildren(
|
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
// Ignore when a variable is used <fbt desc="Greeting">{dynamicValue}</fbt>
|
|
81
|
-
if (
|
|
82
|
-
child.expression.type === 'Identifier' &&
|
|
83
|
-
child.expression.name !== 'undefined'
|
|
84
|
-
) {
|
|
72
|
+
if (child.expression.type === 'Identifier' && child.expression.name !== 'undefined') {
|
|
85
73
|
hasTextContent = true;
|
|
86
74
|
continue;
|
|
87
75
|
}
|
|
@@ -5,10 +5,7 @@ import {
|
|
|
5
5
|
resolveNodeValue,
|
|
6
6
|
} from '../utils.tsx';
|
|
7
7
|
|
|
8
|
-
export default createRule<
|
|
9
|
-
[],
|
|
10
|
-
'emptyDesc' | 'jsxEmptyDesc' | 'duplicateDesc' | 'shortDesc'
|
|
11
|
-
>({
|
|
8
|
+
export default createRule<[], 'emptyDesc' | 'jsxEmptyDesc' | 'duplicateDesc' | 'shortDesc'>({
|
|
12
9
|
create(context) {
|
|
13
10
|
return {
|
|
14
11
|
CallExpression(node) {
|
|
@@ -19,9 +16,7 @@ export default createRule<
|
|
|
19
16
|
const [textArg, descArg] = node.arguments;
|
|
20
17
|
|
|
21
18
|
const desc =
|
|
22
|
-
descArg && descArg.type !== 'SpreadElement'
|
|
23
|
-
? resolveNodeValue(descArg)?.trim()
|
|
24
|
-
: null;
|
|
19
|
+
descArg && descArg.type !== 'SpreadElement' ? resolveNodeValue(descArg)?.trim() : null;
|
|
25
20
|
|
|
26
21
|
if (!desc) {
|
|
27
22
|
context.report({
|
|
@@ -40,9 +35,7 @@ export default createRule<
|
|
|
40
35
|
}
|
|
41
36
|
|
|
42
37
|
const text =
|
|
43
|
-
textArg && textArg.type !== 'SpreadElement'
|
|
44
|
-
? resolveNodeValue(textArg)?.trim()
|
|
45
|
-
: null;
|
|
38
|
+
textArg && textArg.type !== 'SpreadElement' ? resolveNodeValue(textArg)?.trim() : null;
|
|
46
39
|
|
|
47
40
|
if (desc.toLowerCase() === text?.toLowerCase()) {
|
|
48
41
|
context.report({
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
3
|
-
createRule,
|
|
4
|
-
getPropName,
|
|
5
|
-
hasFbtParent,
|
|
6
|
-
isFbtNode,
|
|
7
|
-
resolveNodeValue,
|
|
8
|
-
} from '../utils.tsx';
|
|
2
|
+
import { createRule, getPropName, hasFbtParent, isFbtNode, resolveNodeValue } from '../utils.tsx';
|
|
9
3
|
|
|
10
4
|
type Options = [{ ignoredWords: Array<string> }];
|
|
11
5
|
|
|
@@ -20,8 +14,7 @@ const attributes = new Set([
|
|
|
20
14
|
|
|
21
15
|
const shouldIgnoreParent = (node: TSESTree.Node) => {
|
|
22
16
|
const { parent } = node;
|
|
23
|
-
const openingElement =
|
|
24
|
-
parent?.type === 'JSXElement' ? parent?.openingElement : null;
|
|
17
|
+
const openingElement = parent?.type === 'JSXElement' ? parent?.openingElement : null;
|
|
25
18
|
return (
|
|
26
19
|
openingElement?.type === 'JSXOpeningElement' &&
|
|
27
20
|
openingElement.name.type === 'JSXIdentifier' &&
|
|
@@ -31,9 +24,7 @@ const shouldIgnoreParent = (node: TSESTree.Node) => {
|
|
|
31
24
|
|
|
32
25
|
export default createRule<Options, 'unwrappedString'>({
|
|
33
26
|
create(context, options) {
|
|
34
|
-
const ignoredWords = new Set(
|
|
35
|
-
options[0].ignoredWords.map((value) => value.toLowerCase()),
|
|
36
|
-
);
|
|
27
|
+
const ignoredWords = new Set(options[0].ignoredWords.map((value) => value.toLowerCase()));
|
|
37
28
|
|
|
38
29
|
const isIgnoredWord = (value: string) =>
|
|
39
30
|
ignoredWords.has(value.replaceAll(/\s+/g, ' ').toLowerCase());
|
|
@@ -56,10 +47,7 @@ export default createRule<Options, 'unwrappedString'>({
|
|
|
56
47
|
return;
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
if (
|
|
60
|
-
node.value?.type === 'JSXExpressionContainer' &&
|
|
61
|
-
isFbtNode(node.value.expression)
|
|
62
|
-
) {
|
|
50
|
+
if (node.value?.type === 'JSXExpressionContainer' && isFbtNode(node.value.expression)) {
|
|
63
51
|
return;
|
|
64
52
|
}
|
|
65
53
|
|
|
@@ -131,12 +119,10 @@ export default createRule<Options, 'unwrappedString'>({
|
|
|
131
119
|
|
|
132
120
|
meta: {
|
|
133
121
|
docs: {
|
|
134
|
-
description:
|
|
135
|
-
'Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`',
|
|
122
|
+
description: 'Enforce text content to be wrapped with `<fbt>`, `fbt()` or `fbs()`',
|
|
136
123
|
},
|
|
137
124
|
messages: {
|
|
138
|
-
unwrappedString:
|
|
139
|
-
'Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`',
|
|
125
|
+
unwrappedString: 'Text content must be wrapped in `<fbt>`, `fbt()` or `fbs()`',
|
|
140
126
|
},
|
|
141
127
|
schema: [
|
|
142
128
|
{
|
package/src/utils.tsx
CHANGED
|
@@ -17,9 +17,7 @@ function resolveMemberExpressions(
|
|
|
17
17
|
return `${object.name}.${property.name}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function elementType(
|
|
21
|
-
node: TSESTree.JSXElement | TSESTree.JSXFragment,
|
|
22
|
-
): string {
|
|
20
|
+
export function elementType(node: TSESTree.JSXElement | TSESTree.JSXFragment): string {
|
|
23
21
|
if (node.type === 'JSXFragment') {
|
|
24
22
|
return '<>';
|
|
25
23
|
}
|
|
@@ -65,9 +63,7 @@ export function resolveJsxElementTextContent(
|
|
|
65
63
|
.trim();
|
|
66
64
|
}
|
|
67
65
|
|
|
68
|
-
export function resolveBinaryExpression(
|
|
69
|
-
node: TSESTree.BinaryExpression,
|
|
70
|
-
): string | null {
|
|
66
|
+
export function resolveBinaryExpression(node: TSESTree.BinaryExpression): string | null {
|
|
71
67
|
if (node.operator !== '+') {
|
|
72
68
|
return null;
|
|
73
69
|
}
|