@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.
- package/README.md +220 -562
- package/lib/index.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,90 +1,87 @@
|
|
|
1
1
|
# fbtee
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
<
|
|
10
|
-
<
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
20
|
+
- **Easy Setup:** Quick integration with Babel, SWC, Vite, Next.js, and Expo.
|
|
23
21
|
|
|
24
22
|
## Getting Started
|
|
25
23
|
|
|
26
|
-
|
|
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
|
-
|
|
29
|
+
For an existing app, install the runtime:
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
```bash
|
|
32
|
+
npm install fbtee
|
|
33
|
+
```
|
|
34
34
|
|
|
35
|
-
|
|
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
|
-
###
|
|
37
|
+
### Babel, Vite and React
|
|
39
38
|
|
|
40
|
-
|
|
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
|
-
|
|
45
|
+
With Vite, `@rolldown/plugin-babel`, and `@vitejs/plugin-react`:
|
|
47
46
|
|
|
48
|
-
```
|
|
49
|
-
|
|
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
|
-
|
|
63
|
+
With a direct Babel setup:
|
|
53
64
|
|
|
54
|
-
```
|
|
55
|
-
|
|
65
|
+
```js
|
|
66
|
+
export default {
|
|
67
|
+
presets: ['@nkzw/babel-preset-fbtee'],
|
|
68
|
+
};
|
|
56
69
|
```
|
|
57
70
|
|
|
58
|
-
|
|
71
|
+
With Next.js and Babel:
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
```js
|
|
74
|
+
export default {
|
|
75
|
+
presets: ['next/babel', '@nkzw/babel-preset-fbtee'],
|
|
76
|
+
};
|
|
77
|
+
```
|
|
61
78
|
|
|
62
|
-
|
|
79
|
+
### SWC and Turbopack
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
Install the SWC runtime compiler and the CLI:
|
|
65
82
|
|
|
66
|
-
```
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
109
|
+
### TypeScript JSX Types
|
|
113
110
|
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
+
## Writing Strings
|
|
158
118
|
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
136
|
+
Use `fbs()` when you need a plain string:
|
|
257
137
|
|
|
258
138
|
```tsx
|
|
259
|
-
import {
|
|
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
|
-
|
|
276
|
-
|
|
141
|
+
<input
|
|
142
|
+
placeholder={fbs('Search projects', 'Project search input placeholder')}
|
|
143
|
+
/>;
|
|
277
144
|
```
|
|
278
145
|
|
|
279
|
-
|
|
146
|
+
## Dynamic Content
|
|
280
147
|
|
|
281
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
159
|
+
<fbt desc="Greeting with a linked viewer name">
|
|
160
|
+
Hello, <UserLink user={viewer} />!
|
|
161
|
+
</fbt>
|
|
312
162
|
```
|
|
313
163
|
|
|
314
|
-
|
|
164
|
+
Use `fbt.sameParam()` or `<fbt:same-param>` when the same token appears more than once.
|
|
315
165
|
|
|
316
|
-
|
|
317
|
-
import { useLocaleContext } from 'fbtee';
|
|
166
|
+
## Plurals
|
|
318
167
|
|
|
319
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
## Lists
|
|
370
188
|
|
|
371
|
-
|
|
189
|
+
Use `<fbt:list>` for locale-aware lists:
|
|
372
190
|
|
|
373
191
|
```tsx
|
|
374
|
-
|
|
192
|
+
<fbt desc="People assigned to a task">
|
|
193
|
+
Assigned to <fbt:list items={assignees} name="assigneeList" />.
|
|
194
|
+
</fbt>
|
|
375
195
|
```
|
|
376
196
|
|
|
377
|
-
|
|
197
|
+
The standalone `list()` helper is available for non-React code:
|
|
378
198
|
|
|
379
199
|
```tsx
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
205
|
+
## Enums
|
|
402
206
|
|
|
403
|
-
|
|
207
|
+
Use enums when runtime values map to a fixed set of translatable labels:
|
|
404
208
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
209
|
+
```tsx
|
|
210
|
+
const StatusLabels = {
|
|
211
|
+
done: 'done',
|
|
212
|
+
open: 'open',
|
|
213
|
+
};
|
|
408
214
|
|
|
409
|
-
|
|
215
|
+
<fbt desc="Task status label">
|
|
216
|
+
This task is <fbt:enum enum-range={StatusLabels} value={status} />.
|
|
217
|
+
</fbt>;
|
|
218
|
+
```
|
|
410
219
|
|
|
411
|
-
|
|
220
|
+
For shared enum modules, use the `$FbtEnum` suffix so the collector can resolve them.
|
|
412
221
|
|
|
413
|
-
|
|
222
|
+
## Pronouns and Gender
|
|
414
223
|
|
|
415
|
-
|
|
224
|
+
Use `<fbt:pronoun>` when a phrase depends on a person's gender:
|
|
416
225
|
|
|
417
226
|
```tsx
|
|
418
|
-
|
|
419
|
-
<
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
233
|
+
Supported pronoun types are `subject`, `object`, `possessive`, and `reflexive`.
|
|
428
234
|
|
|
429
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
```tsx
|
|
455
|
-
import { list } from 'fbtee';
|
|
243
|
+
Pass common strings to the compiler as `fbtCommon`:
|
|
456
244
|
|
|
457
|
-
|
|
245
|
+
```js
|
|
246
|
+
{
|
|
247
|
+
fbtCommon: {
|
|
248
|
+
Save: 'Button label for saving changes',
|
|
249
|
+
},
|
|
250
|
+
}
|
|
458
251
|
```
|
|
459
252
|
|
|
460
|
-
|
|
253
|
+
## Runtime Setup
|
|
461
254
|
|
|
462
|
-
|
|
255
|
+
Most React apps should use `createLocaleContext`:
|
|
463
256
|
|
|
464
257
|
```tsx
|
|
465
|
-
|
|
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
|
-
|
|
260
|
+
const availableLanguages = new Map([
|
|
261
|
+
['en-US', 'English'],
|
|
262
|
+
['de-DE', 'Deutsch'],
|
|
263
|
+
['ja-JP', '日本語'],
|
|
264
|
+
]);
|
|
474
265
|
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
|
|
277
|
+
const LocaleContext = createLocaleContext({
|
|
278
|
+
availableLanguages,
|
|
279
|
+
clientLocales: [navigator.language, ...navigator.languages],
|
|
280
|
+
loadLocale,
|
|
281
|
+
});
|
|
479
282
|
|
|
480
|
-
|
|
283
|
+
export const Root = () => (
|
|
284
|
+
<LocaleContext>
|
|
285
|
+
<App />
|
|
286
|
+
</LocaleContext>
|
|
287
|
+
);
|
|
288
|
+
```
|
|
481
289
|
|
|
482
|
-
|
|
290
|
+
Use `useLocaleContext` to read or change the locale:
|
|
483
291
|
|
|
484
292
|
```tsx
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
301
|
+
If you need full control, use `setupLocaleContext` or `setupFbtee` directly.
|
|
494
302
|
|
|
495
|
-
|
|
303
|
+
### Next.js App Router
|
|
496
304
|
|
|
497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
+
## Translation Workflow
|
|
507
310
|
|
|
508
|
-
|
|
311
|
+
Extract source strings:
|
|
509
312
|
|
|
510
313
|
```bash
|
|
511
314
|
pnpm fbtee collect
|
|
512
315
|
```
|
|
513
316
|
|
|
514
|
-
|
|
317
|
+
This writes `source_strings.json`.
|
|
515
318
|
|
|
516
|
-
|
|
319
|
+
Prepare editable translation files:
|
|
517
320
|
|
|
518
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
342
|
+
Use `--dry-run` first to preview file renames.
|
|
608
343
|
|
|
609
|
-
|
|
344
|
+
Commit the human-authored translation files. Ignore generated runtime output:
|
|
610
345
|
|
|
611
|
-
```
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
646
|
-
import { useLocaleContext } from 'fbtee';
|
|
363
|
+
### Coding Agent prompt:
|
|
647
364
|
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
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
|
-
|
|
660
|
-
};
|
|
370
|
+
Remove `"status": "new"` from each completed translation entry.
|
|
661
371
|
```
|
|
662
372
|
|
|
663
|
-
## ESLint
|
|
373
|
+
## ESLint
|
|
664
374
|
|
|
665
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
399
|
+
_fbtee_ is compatible with the core `fbt` programming model:
|
|
742
400
|
|
|
743
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
+
## Examples
|
|
754
410
|
|
|
755
|
-
|
|
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
|
|
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 = "
|
|
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`);
|