@nkzw/eslint-plugin-fbtee 3.1.0 → 4.0.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 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, Babel, SWC, Next.js & Oxc.
13
+ _fbtee_ is a modern continuation of Facebook's `fbt`, rebuilt for TypeScript, ESM, React 19, Vite, Next.js, and 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, Babel, SWC, Oxc, Next.js, and Expo.
20
+ - **Easy Setup:** Quick integration with Vite, Next.js, Oxc, and Expo.
21
21
 
22
22
  ## Getting Started
23
23
 
@@ -62,52 +62,31 @@ export default defineConfig({
62
62
  });
63
63
  ```
64
64
 
65
- ### Babel
65
+ ### Next.js
66
66
 
67
- Install the Babel preset and the CLI:
67
+ Next.js projects should use the native Oxc plugin and the CLI:
68
68
 
69
69
  ```bash
70
- npm install -D @nkzw/babel-preset-fbtee @nkzw/fbtee-cli
70
+ npm install -D @nkzw/next-plugin-fbtee @nkzw/fbtee-cli
71
71
  ```
72
72
 
73
- Add the preset to your Babel configuration:
73
+ Wrap your Next configuration:
74
74
 
75
75
  ```js
76
- export default {
77
- presets: ['@nkzw/babel-preset-fbtee'],
78
- };
79
- ```
80
-
81
- ### SWC
82
-
83
- Install the SWC runtime compiler and the CLI:
84
-
85
- ```bash
86
- npm install -D @nkzw/swc-plugin-fbtee @nkzw/fbtee-cli
87
- ```
88
-
89
- For Next.js SWC plugins:
76
+ import withFbtee from '@nkzw/next-plugin-fbtee';
90
77
 
91
- ```js
92
- import { createFbteePluginOptions } from '@nkzw/swc-plugin-fbtee/index.js';
93
-
94
- export default {
95
- experimental: {
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
- ],
78
+ export default withFbtee({
79
+ fbtCommon: {
80
+ Accept: 'Button label for accepting terms',
107
81
  },
108
- };
82
+ fbtEnumManifest: {},
83
+ })({
84
+ // Your Next.js configuration.
85
+ });
109
86
  ```
110
87
 
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
+
111
90
  ### Low-level Oxc Transform
112
91
 
113
92
  For custom build-system integrations, install the native Oxc transform:
@@ -159,10 +138,7 @@ Use `fbt()` outside JSX:
159
138
  ```tsx
160
139
  import { fbt } from 'fbtee';
161
140
 
162
- const title = fbt(
163
- 'No tasks yet',
164
- 'Empty state title when a project has no tasks',
165
- );
141
+ const title = fbt('No tasks yet', 'Empty state title when a project has no tasks');
166
142
  ```
167
143
 
168
144
  Use `fbs()` when you need a plain string:
@@ -170,9 +146,7 @@ Use `fbs()` when you need a plain string:
170
146
  ```tsx
171
147
  import { fbs } from 'fbtee';
172
148
 
173
- <input
174
- placeholder={fbs('Search projects', 'Project search input placeholder')}
175
- />;
149
+ <input placeholder={fbs('Search projects', 'Project search input placeholder')} />;
176
150
  ```
177
151
 
178
152
  ## Dynamic Content
@@ -202,12 +176,7 @@ Use `<fbt:plural>` when a count controls grammar. _fbtee_ handles locale-specifi
202
176
  ```tsx
203
177
  <fbt desc="Inbox unread count">
204
178
  You have{' '}
205
- <fbt:plural
206
- count={count}
207
- many="unread messages"
208
- name="unreadCount"
209
- showCount="yes"
210
- >
179
+ <fbt:plural count={count} many="unread messages" name="unreadCount" showCount="yes">
211
180
  an unread message
212
181
  </fbt:plural>
213
182
  .
@@ -340,6 +309,17 @@ See the [Next.js fbtee example](https://github.com/cpojer/nextjs-fbtee-example)
340
309
 
341
310
  ## Translation Workflow
342
311
 
312
+ The CLI is a native Rust executable and uses the Oxc compiler pipeline by default:
313
+
314
+ ```bash
315
+ pnpm fbtee collect
316
+ pnpm fbtee prepare-translations --source-strings source_strings.json --output-dir translations
317
+ pnpm fbtee translate --source-strings source_strings.json --translations 'translations/*.json' --output-dir src/translations
318
+ pnpm fbtee migrate-locales --to bcp47 --dir translations --dir src/translations
319
+ ```
320
+
321
+ fbtee 4 removes the legacy Babel compiler and JavaScript CLI. Babel-specific extensions (`--custom-collector`, `--transform`, `--generate-fbt-nodes`, and `--hash-module`) are no longer supported. The collector also cannot execute Babel configuration or apply `.babelignore`; it stops with a clear diagnostic if either is detected. Remove that legacy configuration, or pass `--disable-babel-config` to explicitly collect the unmodified source.
322
+
343
323
  Extract source strings:
344
324
 
345
325
  ```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.1.0";
4
+ var version = "4.0.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.1.0",
3
+ "version": "4.0.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
- 'Empty strings are not allowed in fbt() and fbs() function arguments.',
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
  }