@nkzw/eslint-plugin-fbtee 1.8.0 → 2.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.
Files changed (3) hide show
  1. package/README.md +220 -562
  2. package/lib/index.mjs +1 -1
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,90 +1,87 @@
1
1
  # fbtee
2
2
 
3
- _**fbtee** (Far Better Translations, Extended Edition) is an internationalization framework for JavaScript & React designed to be **powerful**, **flexible**, and **intuitive**._
4
-
5
- fbtee features inline translations and supports dynamic content, pluralization, lists, and more. It is built on top of Facebook's `fbt` library, which has been used in production for over a decade, serving billions of users.
3
+ **_fbtee_** is an internationalization framework for JavaScript and React. It lets you write translatable text inline, keep translator context next to the UI, and compile strings into a small runtime format for production.
6
4
 
7
5
  ```tsx
8
6
  const Greeting = ({ name }) => (
9
- <div>
10
- <fbt desc="Greeting">
11
- Hello, <Name name={name} />!
12
- </fbt>
13
- </div>
7
+ <fbt desc="Greeting on the home screen">
8
+ Hello, <Name name={name} />!
9
+ </fbt>
14
10
  );
15
11
  ```
16
12
 
17
- ## Why choose **fbtee**?
13
+ _fbtee_ is a modern continuation of Facebook's `fbt`, rebuilt for TypeScript, ESM, React 19, Vite, Next.js, Babel & SWC.
14
+
15
+ ## Features
18
16
 
19
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.
20
- - **Proven in Production:** Built on Facebook's `fbt`, with over a decade of production usage, serving billions of users and two years of production usage in [Athena Crisis](https://athenacrisis.com).
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).
21
19
  - **Optimized Performance with IR:** Compiles translations into an Intermediate Representation (IR) for extracting strings, then optimizes the runtime output for performance.
22
- - **Easy Setup:** Quick integration with tools like Babel and Vite means you can get started instantly.
20
+ - **Easy Setup:** Quick integration with Babel, SWC, Vite, Next.js, and Expo.
23
21
 
24
22
  ## Getting Started
25
23
 
26
- Tired of setting up new projects? Check out these templates for web and React Native that come with **fbtee** pre-configured:
24
+ Use one of the templates if you are starting fresh:
27
25
 
28
26
  - [Web App Template](https://github.com/nkzw-tech/web-app-template)
29
27
  - [Expo App Template](https://github.com/nkzw-tech/expo-app-template)
30
28
 
31
- ### Examples
29
+ For an existing app, install the runtime:
32
30
 
33
- Sometimes it's easiest to learn by example and copy-paste the setup from existing projects. Here are some examples of using **fbtee**:
31
+ ```bash
32
+ npm install fbtee
33
+ ```
34
34
 
35
- - [Next.js App fbtee Example](https://github.com/cpojer/nextjs-fbtee-example) - A Next.js App Router example using **fbtee** with Server Components.
36
- - [Athena Crisis](https://github.com/nkzw-tech/athena-crisis) - an open source video game using **fbtee**
35
+ _fbtee_ requires Node 22+. React apps should use React 19+.
37
36
 
38
- ### Installation
37
+ ### Babel, Vite and React
39
38
 
40
- **fbtee** requires at least Node 22, and React 19 if you are using React.
39
+ Install the Babel preset and the Rolldown Babel plugin:
41
40
 
42
41
  ```bash
43
- npm install fbtee
42
+ npm install -D @nkzw/babel-preset-fbtee @rolldown/plugin-babel
44
43
  ```
45
44
 
46
- In addition to `fbtee`, you need to install the compiler integration for your build tool. For Babel-based builds, install the Babel preset. This keeps the existing `fbtee` CLI commands available through the Babel tooling package:
45
+ With Vite, `@rolldown/plugin-babel`, and `@vitejs/plugin-react`:
47
46
 
48
- ```bash
49
- npm install -D @nkzw/babel-preset-fbtee
47
+ ```ts
48
+ import fbteePreset from '@nkzw/babel-preset-fbtee';
49
+ import babel from '@rolldown/plugin-babel';
50
+ import react from '@vitejs/plugin-react';
51
+ import { defineConfig } from 'vite';
52
+
53
+ export default defineConfig({
54
+ plugins: [
55
+ babel({
56
+ presets: [fbteePreset],
57
+ }),
58
+ react(),
59
+ ],
60
+ });
50
61
  ```
51
62
 
52
- For SWC or Turbopack builds, install the SWC plugin and the standalone CLI package:
63
+ With a direct Babel setup:
53
64
 
54
- ```bash
55
- npm install -D @nkzw/swc-plugin-fbtee @nkzw/fbtee-cli
65
+ ```js
66
+ export default {
67
+ presets: ['@nkzw/babel-preset-fbtee'],
68
+ };
56
69
  ```
57
70
 
58
- ### Tooling Setup
71
+ With Next.js and Babel:
59
72
 
60
- #### Using **fbtee** with SWC or Turbopack
73
+ ```js
74
+ export default {
75
+ presets: ['next/babel', '@nkzw/babel-preset-fbtee'],
76
+ };
77
+ ```
61
78
 
62
- Use the SWC plugin when your build pipeline runs through SWC instead of Babel, such as Turbopack. Phrase extraction still uses `fbtee collect`; install `@nkzw/fbtee-cli` for SWC-only projects that do not install the Babel preset. The SWC plugin is the runtime compiler that lowers `fbt` and `<fbt>` callsites during application builds.
79
+ ### SWC and Turbopack
63
80
 
64
- For raw SWC configuration:
81
+ Install the SWC runtime compiler and the CLI:
65
82
 
66
- ```json
67
- {
68
- "jsc": {
69
- "experimental": {
70
- "plugins": [
71
- [
72
- "@nkzw/swc-plugin-fbtee",
73
- {
74
- "fbtCommon": {
75
- "Accept": "Button/Link: Accept conditions"
76
- },
77
- "fbtEnumManifest": {}
78
- }
79
- ]
80
- ]
81
- },
82
- "parser": {
83
- "syntax": "typescript",
84
- "tsx": true
85
- }
86
- }
87
- }
83
+ ```bash
84
+ npm install -D @nkzw/swc-plugin-fbtee @nkzw/fbtee-cli
88
85
  ```
89
86
 
90
87
  For Next.js SWC plugins:
@@ -97,7 +94,7 @@ export default {
97
94
  '@nkzw/swc-plugin-fbtee',
98
95
  {
99
96
  fbtCommon: {
100
- Accept: 'Button/Link: Accept conditions',
97
+ Accept: 'Button label for accepting terms',
101
98
  },
102
99
  fbtEnumManifest: {},
103
100
  },
@@ -107,655 +104,316 @@ export default {
107
104
  };
108
105
  ```
109
106
 
110
- Do not pass `collectFbt: true` to the SWC plugin. Use `fbtee collect` for extraction and the SWC plugin for compiling app code.
107
+ Use the SWC plugin to compile app code. Use `fbtee collect` to extract phrases. Do not pass `collectFbt: true` to the SWC plugin.
111
108
 
112
- #### Using **fbtee** with Vite
109
+ ### TypeScript JSX Types
113
110
 
114
- If you are using Vite, install the React plugin for Vite:
115
-
116
- ```bash
117
- npm install -D @vitejs/plugin-react
118
- ```
119
-
120
- In your `vite.config.ts`:
121
-
122
- ```ts
123
- import fbteePreset from '@nkzw/babel-preset-fbtee';
124
- import react from '@vitejs/plugin-react';
125
-
126
- export default {
127
- plugins: [
128
- react({
129
- babel: {
130
- presets: [fbteePreset],
131
- },
132
- }),
133
- ],
134
- };
135
- ```
136
-
137
- #### Using Babel directly instead of Vite's React Plugin
138
-
139
- If you are not using `@vitejs/plugin-react`, for example, because you are using the latest version of React Router in framework mode, you can use `vite-plugin-babel` instead:
111
+ React TypeScript projects should include the JSX declarations once in a global type file or app entry point:
140
112
 
141
113
  ```tsx
142
- import fbteePreset from '@nkzw/babel-preset-fbtee';
143
- import { reactRouter } from '@react-router/dev/vite';
144
- import { defineConfig } from 'vite';
145
- import babel from 'vite-plugin-babel';
146
-
147
- export default defineConfig({
148
- plugins: [
149
- babel({
150
- babelConfig: { presets: [fbteePreset] },
151
- }),
152
- reactRouter(),
153
- ],
154
- });
114
+ /// <reference types="fbtee/ReactTypes.d.ts" />
155
115
  ```
156
116
 
157
- #### Using **fbtee** with Next.js and Babel
117
+ ## Writing Strings
158
118
 
159
- Create a `babel.config.js` file in the root of your Next.js project and add the **fbtee** preset:
119
+ Every user-facing string should be wrapped in `<fbt>`, `fbt()`, or `fbs()`. Descriptions are required because they are the translator's context.
160
120
 
161
121
  ```tsx
162
- export default {
163
- presets: ['next/babel', '@nkzw/babel-preset-fbtee'],
164
- };
165
- ```
166
-
167
- #### Scripts
168
-
169
- **fbtee** uses two scripts to manage translations. These scripts help automate the process of collecting, creating, and compiling translations.
170
-
171
- - `fbtee collect`: This command reads your source files and extracts all the translatable strings into a `source_strings.json` file. This file should be uploaded to your translation provider as the source for your translations.
172
- - `fbtee translate`: This command takes the collected strings and your translations, and compiles them into an optimized format for use in your application.
173
-
174
- By default, **fbtee** expects your source code to be anywhere within your project, your translations in a `translations` folder, and the generated translations in `src/translations`. You can customize these paths using command line arguments:
175
-
176
- - The `--translations` parameter can be specified to `fbtee translate` to customize the path to the input translation files.
177
- - The `--output-dir` parameter defines where the output translation files per language should be written (one file per locale), so they can be lazy loaded in your app.
178
- - The `--output-file` parameter outputs all translations in a single combined file, which can be used directly in your app without loading individual files.
179
-
180
- If you want to use different paths, it is recommended to define custom commands in your `package.json`:
181
-
182
- ```json
183
- {
184
- "scripts": {
185
- "fbtee:collect": "fbtee collect --src src",
186
- "fbtee:translate": "fbtee translate --translations translations/*.json -o src/translations/"
187
- }
188
- }
122
+ <fbt desc="Empty state title when a project has no tasks">No tasks yet</fbt>
189
123
  ```
190
124
 
191
- Now, run the collect command to set up the initial strings for translation:
192
-
193
- ```bash
194
- pnpm fbtee collect
195
- ```
196
-
197
- Add the files generated by these commands to your `.gitignore`:
198
-
199
- ```
200
- .enum_manifest.json
201
- source_strings.json
202
- src/translations/
203
- ```
204
-
205
- Because no translations exist yet, you can run this command to create an empty file for now:
206
-
207
- ```bash
208
- mkdir -p translations
209
- echo '{"fb-locale": "ja_JP", "translations": {}}' > translations/ja_JP.json
210
- ```
211
-
212
- If you are not using a translation provider, you can also run `fbtee prepare-translations --locales ja_JP` to generate a JSON file that can be edited directly.
213
-
214
- ### App Setup
215
-
216
- **fbtee**'s runtime can manage the currently selected locale for you through the `<LocaleContext />` component. All you need to do is define the available languages, the locales provided by the browser or device, and a function to load translations for a given locale:
125
+ Use `fbt()` outside JSX:
217
126
 
218
127
  ```tsx
219
- import { getLocales } from 'expo-localization';
220
- import { createLocaleContext } from 'fbtee';
221
-
222
- // Define the available languages in your app:
223
- const availableLanguages = new Map([
224
- ['en_US', 'English'],
225
- ['ja_JP', '日本語 (Japanese)'],
226
- ]);
128
+ import { fbt } from 'fbtee';
227
129
 
228
- // Web:
229
- const clientLocalesWeb = [navigator.language, ...navigator.languages];
230
- // React Native:
231
- const clientLocalesRN = getLocales().map(({ languageTag }) => languageTag);
232
-
233
- // A loader function to fetch translations for a given locale:
234
- const loadLocale = async (locale: string) => {
235
- if (locale === 'ja_JP') {
236
- return (await import('./translations/ja_JP.json')).default.ja_JP;
237
- }
238
-
239
- return {};
240
- };
241
-
242
- const LocaleContext = createLocaleContext({
243
- availableLanguages,
244
- clientLocales: clientLocalesWeb, // or clientLocalesRN for React Native
245
- loadLocale,
246
- });
247
-
248
- // Now wrap your app with `LocaleContext`:
249
- const MyAppEntryPoint = () => (
250
- <LocaleContext>
251
- <App />
252
- </LocaleContext>
130
+ const title = fbt(
131
+ 'No tasks yet',
132
+ 'Empty state title when a project has no tasks',
253
133
  );
254
134
  ```
255
135
 
256
- If you need to access the current locale or set the locale, you can use the `useLocaleContext` hook:
136
+ Use `fbs()` when you need a plain string:
257
137
 
258
138
  ```tsx
259
- import { useLocaleContext } from 'fbtee';
260
-
261
- const MyComponent = () => {
262
- const { locale, setLocale } = useLocaleContext();
263
-
264
- return (
265
- <div>
266
- <p>Current Locale: {locale}</p>
267
- <button onClick={() => setLocale('ja_JP')}>Switch to Japanese</button>
268
- </div>
269
- );
270
- };
271
- ```
272
-
273
- Next up, 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`):
139
+ import { fbs } from 'fbtee';
274
140
 
275
- ```tsx
276
- /// <reference types="fbtee/ReactTypes.d.ts" />
141
+ <input
142
+ placeholder={fbs('Search projects', 'Project search input placeholder')}
143
+ />;
277
144
  ```
278
145
 
279
- _You’re now ready to define your first translatable element!_
146
+ ## Dynamic Content
280
147
 
281
- ### Alternative Setup Methods
282
-
283
- If you need to build your own locale context with custom functionality, you can use the `setupLocaleContext` function. It takes the same arguments as the `LocaleContext` component, but gives you full control over how you manage locales.
148
+ Use `<fbt:param>` for dynamic values. Token names should describe the value, not its current English position.
284
149
 
285
150
  ```tsx
286
- const { getLocale, setLocale } = setupLocaleContext({
287
- availableLanguages: AvailableLanguages,
288
- clientLocales,
289
- fallbackLocale,
290
- hooks,
291
- loadLocale,
292
- translations,
293
- });
294
-
295
- // Now you can call `getLocale` and `setLocale` anytime, even outside of React components.
296
- if (getLocale() === 'en_US') {
297
- setLocale('ja_JP'); // Switch to Japanese locale
298
- }
151
+ <fbt desc="Greeting with the viewer name">
152
+ Hello, <fbt:param name="viewerName">{viewer.name}</fbt:param>!
153
+ </fbt>
299
154
  ```
300
155
 
301
- A full example of using `setupLocaleContext` to build your own `LocaleContext` abstraction can be found in the [Athena Crisis](https://github.com/nkzw-tech/athena-crisis/blob/main/hera/i18n/LocaleContext.tsx#L41) repository.
302
-
303
- ### Gender Variations
304
-
305
- `createLocaleContext` and `setupLocaleContext` also support setting the user's gender:
156
+ React elements inside `<fbt>` are automatically turned into implicit params:
306
157
 
307
158
  ```tsx
308
- createLocaleContext({
309
- …
310
- gender: 'female', // 'male', 'female' or 'unknown' are supported.
311
- });
159
+ <fbt desc="Greeting with a linked viewer name">
160
+ Hello, <UserLink user={viewer} />!
161
+ </fbt>
312
162
  ```
313
163
 
314
- If you need to adjust the user's gender dynamically, you can use the `setGender` function provided by the `useLocaleContext` hook:
164
+ Use `fbt.sameParam()` or `<fbt:same-param>` when the same token appears more than once.
315
165
 
316
- ```tsx
317
- import { useLocaleContext } from 'fbtee';
166
+ ## Plurals
318
167
 
319
- const GenderSelector = () => {
320
- const { gender, setGender } = useLocaleContext();
321
-
322
- return (
323
- <div>
324
- <button onClick={() => setGender('male')}>
325
- <fbt desc="Male gender">Male</fbt>
326
- </button>
327
- <button onClick={() => setGender('female')}>
328
- <fbt desc="Female gender">Female</fbt>
329
- </button>
330
- <button onClick={() => setGender('unknown')}>
331
- <fbt desc="Unknown gender">Unknown</fbt>
332
- </button>
333
- </div>
334
- );
335
- };
336
- ```
337
-
338
- #### Full Customization
339
-
340
- Finally, if you are using something other than React, or need even more control over how **fbtee** is configured, you can use the `setupFbtee` function. This function allows you to set up **fbtee** with custom hooks and translations:
168
+ Use `<fbt:plural>` when a count controls grammar. _fbtee_ handles locale-specific plural rules.
341
169
 
342
170
  ```tsx
343
- import { IntlVariations, setupFbtee } from 'fbtee';
344
- import translations from './translations.json';
345
-
346
- setupFbtee({
347
- hooks: {
348
- getViewerContext: () => ({
349
- GENDER: IntlVariations.GENDER_UNKNOWN,
350
- locale: 'en_US',
351
- }),
352
- },
353
- translations,
354
- });
171
+ <fbt desc="Inbox unread count">
172
+ You have{' '}
173
+ <fbt:plural
174
+ count={count}
175
+ many="unread messages"
176
+ name="unreadCount"
177
+ showCount="yes"
178
+ >
179
+ an unread message
180
+ </fbt:plural>
181
+ .
182
+ </fbt>
355
183
  ```
356
184
 
357
- ### Usage with Next.js
358
-
359
- If you are using Next.js with App Router, check out the [Next.js App fbtee Example](https://github.com/cpojer/nextjs-fbtee-example). You'll need to set up a `<LocaleContext />` as a Client Component, and call `setupFbtee` in the root layout of your app for Server Component support.
360
-
361
- When using Next.js with **fbtee**, translated strings are inserted when your Server Components are rendered. This means that changing locales on the client will not automatically update the Server Component's translated strings. To handle locale changes, you need to call a server action that updates the locale, and then re-render your app with the new locale, for example by reloading the page.
362
-
363
- If you are exclusively using **fbtee** within Client Components, you can handle locale changes directly on the client only.
364
-
365
- ## Usage
366
-
367
- _If you want to learn by example, check out the [examples](https://github.com/nkzw-tech/fbtee/tree/main/example) directory. You can also check out [Athena Crisis](https://github.com/nkzw-tech/athena-crisis), a large open source video game using **fbtee**_
185
+ `showCount` can be `yes`, `ifMany`, or `no`.
368
186
 
369
- With **fbtee**, all strings must be wrapped using `<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.
187
+ ## Lists
370
188
 
371
- Here are some basic examples:
189
+ Use `<fbt:list>` for locale-aware lists:
372
190
 
373
191
  ```tsx
374
- const Greeting = () => <div>Hello, World!</div>;
192
+ <fbt desc="People assigned to a task">
193
+ Assigned to <fbt:list items={assignees} name="assigneeList" />.
194
+ </fbt>
375
195
  ```
376
196
 
377
- You can wrap the string with `<fbt>`:
197
+ The standalone `list()` helper is available for non-React code:
378
198
 
379
199
  ```tsx
380
- const Greeting = () => (
381
- <div>
382
- <fbt desc="Greeting">Hello, World!</fbt>
383
- </div>
384
- );
385
- ```
386
-
387
- `<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). Here is a more complex example of what you can do with **fbtee**:
200
+ import { list } from 'fbtee';
388
201
 
389
- ```tsx
390
- <fbt desc="Feed item">
391
- <fbt:list items={users} name="userList" />{' '}
392
- won the game against
393
- <fbt:plural count={bots} many="bots" name="numberOfBots" showCount="ifMany">
394
- a bot
395
- </fbt:plural> on the map
396
- <fbt:param name="mapName">{game.mapName}</fbt:param>.
397
- </fbt:plural>
398
- // Example Output: "Alice, Bob and Charlie won the game against 2 bots on the map Forest Battle."
202
+ const names = list(['Alice', 'Bob', 'Charlie'], 'or', 'comma');
399
203
  ```
400
204
 
401
- _Note: `<fbt>` is auto-imported for you by the fbtee compiler integration._
205
+ ## Enums
402
206
 
403
- This example highlights several features of **fbtee**:
207
+ Use enums when runtime values map to a fixed set of translatable labels:
404
208
 
405
- - `<fbt:list>`: Renders a list of items, automatically handling conjunctions and delimiters.
406
- - `<fbt:plural>`: Handles singular and plural forms based on a count value.
407
- - `<fbt:param>`: Allows you to insert dynamic content into your strings, such as variables or React components.
209
+ ```tsx
210
+ const StatusLabels = {
211
+ done: 'done',
212
+ open: 'open',
213
+ };
408
214
 
409
- With the above components, you can handle complex strings that are translatable into multiple languages while maintaining clarity and context for translators. This sentence structure supports many variations, such as "Alice is playing on Forest Battle.", or "Alice, Bob and Charlie are playing on Giant Crisis".
215
+ <fbt desc="Task status label">
216
+ This task is <fbt:enum enum-range={StatusLabels} value={status} />.
217
+ </fbt>;
218
+ ```
410
219
 
411
- Let's look at each of these features in detail:
220
+ For shared enum modules, use the `$FbtEnum` suffix so the collector can resolve them.
412
221
 
413
- ### Dynamic Content
222
+ ## Pronouns and Gender
414
223
 
415
- **fbtee** supports dynamic content through <fbt:param> or by using other React components within your text. For example, if you want to greet a specific user based on a name passed as a prop, you can use `<fbt:param>`:
224
+ Use `<fbt:pronoun>` when a phrase depends on a person's gender:
416
225
 
417
226
  ```tsx
418
- const Greeting = ({ name }) => (
419
- <div>
420
- <fbt desc="Greeting">
421
- Hello, <fbt:param name="name">{name}</fbt:param>!
422
- </fbt>
423
- </div>
424
- );
227
+ <fbt desc="Photo sharing notification">
228
+ <fbt:param name="name">{user.name}</fbt:param> shared{' '}
229
+ <fbt:pronoun gender={user.gender} human type="possessive" /> photo.
230
+ </fbt>
425
231
  ```
426
232
 
427
- **fbtee** allows you to use regular React Components inside of `<fbt>` which will automatically create `<fbt:param>` calls for you:
233
+ Supported pronoun types are `subject`, `object`, `possessive`, and `reflexive`.
428
234
 
429
- ```tsx
430
- const Greeting = ({ name }) => (
431
- <div>
432
- <fbt desc="Greeting">
433
- Hello, <Name name={name} />!
434
- </fbt>
435
- </div>
436
- );
437
- ```
235
+ ## Common Strings
438
236
 
439
- ### Lists
440
-
441
- `<fbt:list>` is used to render a list of items with conjunctions and delimiters. It automatically handles the grammatical structure of the list based on the locale:
237
+ Common strings are reusable source strings with shared descriptions:
442
238
 
443
239
  ```tsx
444
- <fbt:list
445
- conjunction="or"
446
- delimiter="comma"
447
- items={['Alice', 'Bob', <CharlieNameExampleComponent key="charlieUserID" />]}
448
- name="userList"
449
- /> // "Alice, Bob, or Charlie"
240
+ <fbt common>Save</fbt>
450
241
  ```
451
242
 
452
- The default conjunction is "and", and the default delimiter is "comma". You can use "or" or "none" as conjunction, and "bullet", "comma", or "semicolon" as delimiters. The `name` attribute is required to provide context for translators. **fbtee** also exports a `list` function that can be used in contexts outside of React:
453
-
454
- ```tsx
455
- import { list } from 'fbtee';
243
+ Pass common strings to the compiler as `fbtCommon`:
456
244
 
457
- const userList = list(['Alice', 'Bob', 'Charlie'], 'or', 'comma');
245
+ ```js
246
+ {
247
+ fbtCommon: {
248
+ Save: 'Button label for saving changes',
249
+ },
250
+ }
458
251
  ```
459
252
 
460
- ### Singular & Plural Forms
253
+ ## Runtime Setup
461
254
 
462
- **fbtee** supports singular and plural forms using the `<fbt:plural>` component. While in English, the plural form is often just an "s" at the end of a word, in other languages, it can be more complex. **fbtee** handles this automatically based on the count value you provide. Here's an example:
255
+ Most React apps should use `createLocaleContext`:
463
256
 
464
257
  ```tsx
465
- <fbt desc="end turn confirmation">
466
- Do you want to play against
467
- <fbt:plural count={bots} many="bots" name="numberOfBots" showCount="ifMany">
468
- a bot
469
- </fbt:plural>?
470
- </fbt>
471
- ```
258
+ import { createLocaleContext } from 'fbtee';
472
259
 
473
- This sentence might be translated to one of the following variations:
260
+ const availableLanguages = new Map([
261
+ ['en-US', 'English'],
262
+ ['de-DE', 'Deutsch'],
263
+ ['ja-JP', '日本語'],
264
+ ]);
474
265
 
475
- - "Do you want to play against 1 bot?"
476
- - "Do you want to play against 2 bots?"
266
+ const loadLocale = async (locale: string) => {
267
+ switch (locale) {
268
+ case 'de-DE':
269
+ return (await import('./translations/de-DE.json')).default['de-DE'];
270
+ case 'ja-JP':
271
+ return (await import('./translations/ja-JP.json')).default['ja-JP'];
272
+ default:
273
+ return {};
274
+ }
275
+ };
477
276
 
478
- `<fbt:plural>` expects the singular phrase as children, and an item `count` as prop. You can provide the plural form using the `many` prop. The `showCount` prop can be used to control whether the count should be displayed in the string. `ifMany` only shows the count in the plural case, while `yes` shows it in all cases, and `no` hides it completely. The `name` prop is required to provide context for translators.
277
+ const LocaleContext = createLocaleContext({
278
+ availableLanguages,
279
+ clientLocales: [navigator.language, ...navigator.languages],
280
+ loadLocale,
281
+ });
479
282
 
480
- ### Using Pronouns
283
+ export const Root = () => (
284
+ <LocaleContext>
285
+ <App />
286
+ </LocaleContext>
287
+ );
288
+ ```
481
289
 
482
- You can use `<fbt:pronoun>` to handle pronouns in your strings. This is particularly useful for gendered languages where the pronoun changes based on the subject:
290
+ Use `useLocaleContext` to read or change the locale:
483
291
 
484
292
  ```tsx
485
- <fbt desc="Photo sharing text">
486
- <fbt:param name="name">{user.name}</fbt:param>
487
- shared
488
- <fbt:pronoun type="possessive" gender={user.pronounGender} human />
489
- photo with you.
490
- </fbt>
293
+ import { useLocaleContext } from 'fbtee';
294
+
295
+ const LanguageButton = () => {
296
+ const { locale, setLocale } = useLocaleContext();
297
+ return <button onClick={() => setLocale('de-DE')}>{locale}</button>;
298
+ };
491
299
  ```
492
300
 
493
- ### Plain Text Usage
301
+ If you need full control, use `setupLocaleContext` or `setupFbtee` directly.
494
302
 
495
- In some situations, you may want to use **fbtee** to represent plain text strings without any React components, for example when using alert dialogs or HTML attributes like `placeholder`. For these situations, you can use the `fbs()` function, which is a subset of `fbt` that only supports plain text.
303
+ ### Next.js App Router
496
304
 
497
- ```tsx
498
- import { fbs } from 'fbtee';
305
+ For App Router, put the locale context in a Client Component. If you render translated strings in Server Components, call `setupFbtee` on the server and re-render the route when the locale changes. Client-only strings can switch locale fully on the client.
499
306
 
500
- <input
501
- type="text"
502
- placeholder={fbs('Enter your name', 'Placeholder for name input')}
503
- />;
504
- ```
307
+ See the [Next.js fbtee example](https://github.com/cpojer/nextjs-fbtee-example) for a complete setup.
505
308
 
506
- ### Extracting Strings & Translating
309
+ ## Translation Workflow
507
310
 
508
- After marking your strings for translation with `<fbt>`, run the following commands to extract, and compile translations:
311
+ Extract source strings:
509
312
 
510
313
  ```bash
511
314
  pnpm fbtee collect
512
315
  ```
513
316
 
514
- You can now upload the `source_strings.json` file to your translation provider, for example [Crowdin](https://crowdin.com/). If you'd like to manually edit the strings, you can use the `fbtee prepare-translations --locales de_DE` to generate a JSON file that can be edited directly. Pass `--sort-by-hash` to keep entries in a deterministic order for cleaner diffs when the file is committed.
317
+ This writes `source_strings.json`.
515
318
 
516
- #### Generated File Structure
319
+ Prepare editable translation files:
517
320
 
518
- Your `source_strings.json` might look like this:
519
-
520
- ```json
521
- {
522
- "childParentMappings": {},
523
- "phrases": [
524
- {
525
- "hashToLeaf": {
526
- "MB6OYuvCF1VzjOmqinI42g==": {
527
- "desc": "What's next question",
528
- "text": "What's next?"
529
- }
530
- },
531
- "col_beg": 12,
532
- "col_end": 68,
533
- "filepath": "app/welcome/welcome.tsx",
534
- "line_beg": 43,
535
- "line_end": 43,
536
- "project": "",
537
- "jsfbt": {
538
- "m": [],
539
- "t": {
540
- "desc": "What's next question",
541
- "text": "What's next?"
542
- }
543
- }
544
- }
545
- ]
546
- }
321
+ ```bash
322
+ pnpm fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE fr-FR ja-JP
547
323
  ```
548
324
 
549
- Based on the above source string, the US English translation (ie. `translations/en_US.json`) might look like this:
325
+ `prepare-translations` merges source strings into existing locale files, preserves translated entries, and marks new work with `"status": "new"`. New files use BCP 47 locale identifiers by default, such as `de-DE.json` and `es-419.json`.
550
326
 
551
- ```json
552
- {
553
- "fb-locale": "en_US",
554
- "translations": {
555
- "MB6OYuvCF1VzjOmqinI42g==": {
556
- "tokens": [],
557
- "types": [],
558
- "translations": [
559
- {
560
- "translation": "What's next?",
561
- "variations": []
562
- }
563
- ]
564
- }
565
- }
566
- }
567
- ```
568
-
569
- And German (`translations/de_DE.json`) might look like this:
570
-
571
- ```json
572
- {
573
- "fb-locale": "de_DE",
574
- "translations": {
575
- "MB6OYuvCF1VzjOmqinI42g==": {
576
- "tokens": [],
577
- "types": [],
578
- "translations": [
579
- {
580
- "translation": "Was kommt jetzt?",
581
- "variations": []
582
- }
583
- ]
584
- }
585
- }
586
- }
587
- ```
588
-
589
- Once you have the translated strings stored in a `translations/` folder as JSON files using the locale name, for example `en_US.json` or `de_DE.json`, you can run the following command to generate the translations for use in your app:
327
+ Compile translations for the app:
590
328
 
591
329
  ```bash
592
- pnpm fbtee translate
330
+ pnpm fbtee translate --source-strings source_strings.json --translations 'translations/*.json' --output-dir src/translations
593
331
  ```
594
332
 
595
- By default, **fbtee** outputs the generated translations in `src/translations`. It will create one JSON file per locale. For example, if you have `translations/en_US.json` and `translations/de_DE.json`, it will generate `src/translations/en_US.json` and `src/translations/de_DE.json`. These files are an optimized representation of your translation file, ready to be used in your app:
333
+ fbtee accepts both modern BCP 47 locale identifiers (`de-DE`, `es-419`) and legacy Facebook-style identifiers (`de_DE`, `es_LA`). If both aliasing files exist, for example `translations/de_DE.json` and `translations/de-DE.json`, fbtee throws and asks you to keep one. Existing legacy files are updated in place; new generated files default to BCP 47. To force a specific output style, pass `--output-locale-style=bcp47`, `--output-locale-style=legacy`, or `--output-locale-style=preserve`.
596
334
 
597
- ```json
598
- {
599
- "de_DE": {
600
- "2HVYhv": "Was kommt jetzt?"
601
- }
602
- }
603
- ```
335
+ To migrate editable translation files and generated runtime files to BCP 47 names:
604
336
 
605
- After generating the translation files, your app is ready to display translated content in other languages. Since the `src/translations` folder is auto-generated, it should be ignored by version control and be part of your build process.
337
+ ```bash
338
+ pnpm fbtee migrate-locales --to bcp47 --dir translations --dir src/translations
339
+ pnpm fbtee translate --output-locale-style=bcp47
340
+ ```
606
341
 
607
- ## Setting the initial locale
342
+ Use `--dry-run` first to preview file renames.
608
343
 
609
- If you know the user's locale ahead of time, you can set it when creating the `LocaleContext` by passing the locale as the first item in the `clientLocales` array:
344
+ Commit the human-authored translation files. Ignore generated runtime output:
610
345
 
611
- ```tsx
612
- // Read the user locale from localStorage etc.
613
- const userLocale = localStorage.getItem('locale');
614
- const LocaleContext = createLocaleContext({
615
- availableLanguages,
616
- clientLocales: [userLocale, navigator.language, ...navigator.languages],
617
- loadLocale,
618
- });
346
+ ```gitignore
347
+ .enum_manifest.json
348
+ source_strings.json
349
+ src/translations/
619
350
  ```
620
351
 
621
- Note that if you are picking the locale ahead of time like this, you need to manually load the translations. This is because the `loadLocale` function is only called when the locale changes, and not when the context is first created. This gives you full control over your app's startup:
622
-
623
- ```tsx
624
- const loadLocale = async (locale: string) => {
625
- if (locale === 'ja_JP') {
626
- return (await import('./translations/ja_JP.json')).default.ja_JP;
627
- }
628
- return {};
629
- };
352
+ ## Translating Strings with Coding Agents
630
353
 
631
- const translations = {
632
- [userLocale]: await loadLocale(userLocale),
633
- };
354
+ Coding agents are great at updating _fbtee_ translation files because all the context is in the repository:
634
355
 
635
- const LocaleContext = createLocaleContext({
636
- availableLanguages,
637
- clientLocales: [userLocale, navigator.language, ...navigator.languages],
638
- loadLocale,
639
- translations,
640
- });
641
- ```
356
+ 1. `fbtee prepare-translations` adds every missing translation and marks it with `"status": "new"`.
357
+ 1. The agent edits only entries with `"status": "new"`.
358
+ 1. The agent removes `"status": "new"` after the translation is complete.
359
+ 1. Code review shows a clean diff of the new localized strings.
642
360
 
643
- If you have to wait for data from your server to determine the user's locale, you can use the `setLocale` function provided by `useLocaleContext` to set the locale after the initial render:
361
+ This works best when the app already has translated strings. The agent can infer tone, voice, capitalization, punctuation, and product vocabulary from the existing locale files.
644
362
 
645
- ```tsx
646
- import { useLocaleContext } from 'fbtee';
363
+ ### Coding Agent prompt:
647
364
 
648
- const Root = () => {
649
- // Fetch the locale from the server.
650
- const userLocale = useUserLocale();
651
- const { locale, setLocale } = useLocaleContext();
365
+ ```md
366
+ Run `fbtee prepare-translations --source-strings source_strings.json --output-dir ares/translations --locales de-DE fr-FR ja-JP pl-PL ru-RU zh-CN es-ES it-IT ko-KR pt-BR uk-UA` for all the translations the app supports.
652
367
 
653
- useEffect(() => {
654
- if (locale !== userLocale) {
655
- setLocale(userLocale);
656
- }
657
- }, [locale, setLocale, userLocale]);
368
+ Look at all updated translation files. For every entry with `"status": "new"`, write a translation that matches the tone, voice, and language already used in the app and in the current locale.
658
369
 
659
- return children;
660
- };
370
+ Remove `"status": "new"` from each completed translation entry.
661
371
  ```
662
372
 
663
- ## ESLint Plugin
373
+ ## ESLint
664
374
 
665
- You can install the optional ESLint plugin to catch common mistakes and enforce best practices when using **fbtee**:
375
+ Install the optional ESLint plugin:
666
376
 
667
377
  ```bash
668
378
  npm install -D @nkzw/eslint-plugin-fbtee
669
379
  ```
670
380
 
671
- Add the following configuration to your ESLint configuration:
381
+ Use the recommended config:
672
382
 
673
383
  ```js
674
384
  import fbtee from '@nkzw/eslint-plugin-fbtee';
675
385
 
676
- export default [
677
- fbtee.configs.recommended,
678
- {
679
- plugins: {
680
- '@nkzw/fbtee': fbtee,
681
- },
682
- },
683
- ];
684
- ```
685
-
686
- If you want stricter enforcement of translation rules, you can use the strict configuration, which enables the `no-untranslated-strings` rule. This ensures that all strings in your codebase are marked for translation.
687
-
688
- ```js
689
- import fbtee from '@nkzw/eslint-plugin-fbtee';
690
-
691
- export default [
692
- fbtee.configs.strict,
693
- {
694
- plugins: {
695
- '@nkzw/fbtee': fbtee,
696
- },
697
- },
698
- ];
386
+ export default [fbtee.configs.recommended];
699
387
  ```
700
388
 
701
- Alternatively, if you'd like more granular control over the rules:
389
+ Use the strict config if you want every user-facing string to be wrapped:
702
390
 
703
391
  ```js
704
392
  import fbtee from '@nkzw/eslint-plugin-fbtee';
705
393
 
706
- export default [
707
- fbtee,
708
- {
709
- plugins: {
710
- '@nkzw/fbtee': fbtee,
711
- },
712
- rules: {
713
- '@nkzw/fbtee/no-empty-strings': [
714
- 'error',
715
- {
716
- ignoredWords: ['Banana', 'pnpm install fbtee'],
717
- },
718
- ],
719
- '@nkzw/fbtee/no-unhelpful-desc': 'error',
720
- '@nkzw/fbtee/no-untranslated-strings': 'error',
721
- },
722
- },
723
- ];
394
+ export default [fbtee.configs.strict];
724
395
  ```
725
396
 
726
- ## What's better about `fbtee` than `fbt`?
727
-
728
- 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.
729
-
730
- 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:
731
-
732
- - **Easier Setup:** fbtee works with modern tools like Vite.
733
- - **Statically Typed:** The fbtee compiler ensures correct usage of fbtee, library TypeScript types are provided, and an eslint plugin helps fix common mistakes.
734
- - **Improved React Compatibility:** Removed React-specific hacks and added support for implicit React fragments (`<>`).
735
- - **Enhanced Features:** Fixed and exported `intlList` as a new `<fbt:list>` construct, which was not functional in the original `fbt`.
736
- - **Modernized Codebase:** Rewritten using TypeScript, ES modules (ESM), eslint, and modern JavaScript standards. Removed cruft and legacy code.
737
- - **Updated Tooling:** Uses modern tools like pnpm, Vite, and esbuild for faster and more efficient development of **fbtee**.
738
-
739
- **fbtee** remains compatible with `fbt` and migration is straightforward.
397
+ ## Migration from fbt
740
398
 
741
- ## Migration from `fbt`
399
+ _fbtee_ is compatible with the core `fbt` programming model:
742
400
 
743
- **fbtee** is compatible with `fbt`. If you are already using `fbt`, migrating to fbtee is straightforward:
401
+ 1. Replace `fbt` packages with `fbtee` and the matching compiler package.
402
+ 1. Replace imports from `fbt` with imports from `fbtee`.
403
+ 1. Use `fbtee collect`, `fbtee prepare-translations`, and `fbtee translate`.
404
+ 1. Convert CommonJS common strings or enum modules to ESM when needed.
405
+ 1. Replace legacy setup calls with `setupFbtee`, `createLocaleContext`, or `setupLocaleContext`.
744
406
 
745
- - Follow the "Getting Started" guide above and remove all "fbt" related packages.
746
- - Make sure you are using React 19.
747
- - Replace `import { fbt } from 'fbt'` with `import { fbt } from 'fbtee'`.
748
- - Rename commands from `fbt-collect`, and `fbt-translate` to `fbtee-collect`, and `fbtee-translate`. `fbt manifest` was removed and `fbtee collect` now handles this step automatically.
749
- - If you were using CommonJS modules for common strings or enums, convert them to ES modules.
750
- - Ensure you are using the latest version of Node.js 22 or later.
751
- - Rename your `init({})` call to `setupFbtee({})`.
407
+ Some archived `fbt` options and legacy behaviors were intentionally removed. The compiler errors should point to the modern replacement when one exists.
752
408
 
753
- After these changes, your project should work seamlessly with **fbtee**.
409
+ ## Examples
754
410
 
755
- _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)._
411
+ - [Example App](https://github.com/nkzw-tech/fbtee/tree/main/example)
412
+ - [Next.js App Router Example](https://github.com/cpojer/nextjs-fbtee-example)
413
+ - [Athena Crisis](https://github.com/nkzw-tech/athena-crisis)
756
414
 
757
415
  ## Credits
758
416
 
759
417
  - `fbt` was originally created by [Facebook](https://github.com/facebook/fbt).
760
418
  - The auto-import plugin was created by [@alexandernanberg](https://github.com/alexandernanberg).
761
- - [Nakazawa Tech](https://nkzw.tech) rewrote `fbt` into `fbtee` and continues to maintain this project.
419
+ - [Nakazawa Tech](https://nkzw.tech) rewrote `fbt` into _fbtee_ and maintains this project.
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 = "1.8.0";
4
+ var version = "2.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@nkzw/eslint-plugin-fbtee",
3
- "version": "1.8.0",
3
+ "version": "2.0.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {