@nkzw/eslint-plugin-fbtee 3.0.1 → 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 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, Next.js, Babel & SWC.
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 Babel, SWC, Vite, Next.js, and Expo.
20
+ - **Easy Setup:** Quick integration with Vite, Next.js, Babel, Oxc, and Expo.
21
21
 
22
22
  ## Getting Started
23
23
 
@@ -34,77 +34,104 @@ npm install fbtee
34
34
 
35
35
  _fbtee_ requires Node 22+. React apps should use React 19+.
36
36
 
37
- ### Babel, Vite and React
37
+ ### Vite
38
38
 
39
- Install the Babel preset and the Rolldown Babel plugin:
39
+ Most projects should install the ready-to-use Vite plugin and the CLI:
40
40
 
41
41
  ```bash
42
- npm install -D @nkzw/babel-preset-fbtee @rolldown/plugin-babel
42
+ npm install -D @nkzw/vite-plugin-fbtee @nkzw/fbtee-cli
43
43
  ```
44
44
 
45
- With Vite, `@rolldown/plugin-babel`, and `@vitejs/plugin-react`:
45
+ Add the fbtee plugin before the React plugin:
46
46
 
47
- ```ts
48
- import fbteePreset from '@nkzw/babel-preset-fbtee';
49
- import babel from '@rolldown/plugin-babel';
47
+ ```js
48
+ import fbtee from '@nkzw/vite-plugin-fbtee';
50
49
  import react from '@vitejs/plugin-react';
51
50
  import { defineConfig } from 'vite';
52
51
 
53
52
  export default defineConfig({
54
53
  plugins: [
55
- babel({
56
- presets: [fbteePreset],
54
+ fbtee({
55
+ fbtCommon: {
56
+ Accept: 'Button label for accepting terms',
57
+ },
58
+ fbtEnumManifest: {},
57
59
  }),
58
60
  react(),
59
61
  ],
60
62
  });
61
63
  ```
62
64
 
63
- With a direct Babel setup:
65
+ ### Next.js
66
+
67
+ Next.js projects should use the native Oxc plugin and the CLI:
68
+
69
+ ```bash
70
+ npm install -D @nkzw/next-plugin-fbtee @nkzw/fbtee-cli
71
+ ```
72
+
73
+ Wrap your Next configuration:
64
74
 
65
75
  ```js
66
- export default {
67
- presets: ['@nkzw/babel-preset-fbtee'],
68
- };
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
+ });
86
+ ```
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
+
90
+ ### Babel
91
+
92
+ Install the Babel preset and the CLI:
93
+
94
+ ```bash
95
+ npm install -D @nkzw/babel-preset-fbtee @nkzw/fbtee-cli
69
96
  ```
70
97
 
71
- With Next.js and Babel:
98
+ Add the preset to your Babel configuration:
72
99
 
73
100
  ```js
74
101
  export default {
75
- presets: ['next/babel', '@nkzw/babel-preset-fbtee'],
102
+ presets: ['@nkzw/babel-preset-fbtee'],
76
103
  };
77
104
  ```
78
105
 
79
- ### SWC and Turbopack
106
+ ### Low-level Oxc Transform
80
107
 
81
- Install the SWC runtime compiler and the CLI:
108
+ For custom build-system integrations, install the native Oxc transform:
82
109
 
83
110
  ```bash
84
- npm install -D @nkzw/swc-plugin-fbtee @nkzw/fbtee-cli
111
+ npm install -D @nkzw/oxc-transform-fbtee @nkzw/fbtee-cli oxc-transform
85
112
  ```
86
113
 
87
- For Next.js SWC plugins:
114
+ Apply the fbtee transform before the standard Oxc transform:
88
115
 
89
116
  ```js
90
- export default {
91
- experimental: {
92
- swcPlugins: [
93
- [
94
- '@nkzw/swc-plugin-fbtee',
95
- {
96
- fbtCommon: {
97
- Accept: 'Button label for accepting terms',
98
- },
99
- fbtEnumManifest: {},
100
- },
101
- ],
102
- ],
117
+ import { transformSync as transformFbtee } from '@nkzw/oxc-transform-fbtee';
118
+ import { transformSync as transformOxc } from 'oxc-transform';
119
+
120
+ const fbteeResult = transformFbtee(filename, sourceText, {
121
+ fbtCommon: {
122
+ Accept: 'Button label for accepting terms',
103
123
  },
104
- };
105
- ```
124
+ fbtEnumManifest: {},
125
+ });
126
+
127
+ if (fbteeResult.errors.length > 0) {
128
+ throw new Error(fbteeResult.errors.map(({ message }) => message).join('\n'));
129
+ }
106
130
 
107
- Use the SWC plugin to compile app code. Use `fbtee collect` to extract phrases. Do not pass `collectFbt: true` to the SWC plugin.
131
+ const result = transformOxc(filename, fbteeResult.code, {
132
+ jsx: { runtime: 'automatic' },
133
+ });
134
+ ```
108
135
 
109
136
  ### TypeScript JSX Types
110
137
 
@@ -127,10 +154,7 @@ Use `fbt()` outside JSX:
127
154
  ```tsx
128
155
  import { fbt } from 'fbtee';
129
156
 
130
- const title = fbt(
131
- 'No tasks yet',
132
- 'Empty state title when a project has no tasks',
133
- );
157
+ const title = fbt('No tasks yet', 'Empty state title when a project has no tasks');
134
158
  ```
135
159
 
136
160
  Use `fbs()` when you need a plain string:
@@ -138,9 +162,7 @@ Use `fbs()` when you need a plain string:
138
162
  ```tsx
139
163
  import { fbs } from 'fbtee';
140
164
 
141
- <input
142
- placeholder={fbs('Search projects', 'Project search input placeholder')}
143
- />;
165
+ <input placeholder={fbs('Search projects', 'Project search input placeholder')} />;
144
166
  ```
145
167
 
146
168
  ## Dynamic Content
@@ -170,12 +192,7 @@ Use `<fbt:plural>` when a count controls grammar. _fbtee_ handles locale-specifi
170
192
  ```tsx
171
193
  <fbt desc="Inbox unread count">
172
194
  You have{' '}
173
- <fbt:plural
174
- count={count}
175
- many="unread messages"
176
- name="unreadCount"
177
- showCount="yes"
178
- >
195
+ <fbt:plural count={count} many="unread messages" name="unreadCount" showCount="yes">
179
196
  an unread message
180
197
  </fbt:plural>
181
198
  .
@@ -308,6 +325,19 @@ See the [Next.js fbtee example](https://github.com/cpojer/nextjs-fbtee-example)
308
325
 
309
326
  ## Translation Workflow
310
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
+
311
341
  Extract source strings:
312
342
 
313
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.0.1";
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.0.1",
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
- '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
  }