@nkzw/vite-plugin-fbtee 3.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Nakazawa Tech & Meta Platforms, Inc. and affiliates.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,451 @@
1
+ # fbtee
2
+
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.
4
+
5
+ ```tsx
6
+ const Greeting = ({ name }) => (
7
+ <fbt desc="Greeting on the home screen">
8
+ Hello, <Name name={name} />!
9
+ </fbt>
10
+ );
11
+ ```
12
+
13
+ _fbtee_ is a modern continuation of Facebook's `fbt`, rebuilt for TypeScript, ESM, React 19, Vite, Babel, SWC, Next.js & Oxc.
14
+
15
+ ## Features
16
+
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
+ - **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
+ - **Optimized Performance with IR:** Compiles translations into an Intermediate Representation (IR) for extracting strings, then optimizes the runtime output for performance.
20
+ - **Easy Setup:** Quick integration with Vite, Babel, SWC, Oxc, Next.js, and Expo.
21
+
22
+ ## Getting Started
23
+
24
+ Use one of the templates if you are starting fresh:
25
+
26
+ - [Web App Template](https://github.com/nkzw-tech/web-app-template)
27
+ - [Expo App Template](https://github.com/nkzw-tech/expo-app-template)
28
+
29
+ For an existing app, install the runtime:
30
+
31
+ ```bash
32
+ npm install fbtee
33
+ ```
34
+
35
+ _fbtee_ requires Node 22+. React apps should use React 19+.
36
+
37
+ ### Vite
38
+
39
+ Most projects should install the ready-to-use Vite plugin and the CLI:
40
+
41
+ ```bash
42
+ npm install -D @nkzw/vite-plugin-fbtee @nkzw/fbtee-cli
43
+ ```
44
+
45
+ Add the fbtee plugin before the React plugin:
46
+
47
+ ```js
48
+ import fbtee from '@nkzw/vite-plugin-fbtee';
49
+ import react from '@vitejs/plugin-react';
50
+ import { defineConfig } from 'vite';
51
+
52
+ export default defineConfig({
53
+ plugins: [
54
+ fbtee({
55
+ fbtCommon: {
56
+ Accept: 'Button label for accepting terms',
57
+ },
58
+ fbtEnumManifest: {},
59
+ }),
60
+ react(),
61
+ ],
62
+ });
63
+ ```
64
+
65
+ ### Babel
66
+
67
+ Install the Babel preset and the CLI:
68
+
69
+ ```bash
70
+ npm install -D @nkzw/babel-preset-fbtee @nkzw/fbtee-cli
71
+ ```
72
+
73
+ Add the preset to your Babel configuration:
74
+
75
+ ```js
76
+ export default {
77
+ presets: ['@nkzw/babel-preset-fbtee'],
78
+ };
79
+ ```
80
+
81
+ ### SWC
82
+
83
+ Install the SWC runtime compiler and the CLI:
84
+
85
+ ```bash
86
+ npm install -D @nkzw/swc-plugin-fbtee @nkzw/fbtee-cli
87
+ ```
88
+
89
+ For Next.js SWC plugins:
90
+
91
+ ```js
92
+ import { createFbteePluginOptions } from '@nkzw/swc-plugin-fbtee/index.js';
93
+
94
+ export default {
95
+ experimental: {
96
+ swcPlugins: [
97
+ [
98
+ '@nkzw/swc-plugin-fbtee',
99
+ createFbteePluginOptions({
100
+ fbtCommon: {
101
+ Accept: 'Button label for accepting terms',
102
+ },
103
+ fbtEnumManifest: {},
104
+ }),
105
+ ],
106
+ ],
107
+ },
108
+ };
109
+ ```
110
+
111
+ ### Low-level Oxc Transform
112
+
113
+ For custom build-system integrations, install the native Oxc transform:
114
+
115
+ ```bash
116
+ npm install -D @nkzw/oxc-transform-fbtee @nkzw/fbtee-cli oxc-transform
117
+ ```
118
+
119
+ Apply the fbtee transform before the standard Oxc transform:
120
+
121
+ ```js
122
+ import { transformSync as transformFbtee } from '@nkzw/oxc-transform-fbtee';
123
+ import { transformSync as transformOxc } from 'oxc-transform';
124
+
125
+ const fbteeResult = transformFbtee(filename, sourceText, {
126
+ fbtCommon: {
127
+ Accept: 'Button label for accepting terms',
128
+ },
129
+ fbtEnumManifest: {},
130
+ });
131
+
132
+ if (fbteeResult.errors.length > 0) {
133
+ throw new Error(fbteeResult.errors.map(({ message }) => message).join('\n'));
134
+ }
135
+
136
+ const result = transformOxc(filename, fbteeResult.code, {
137
+ jsx: { runtime: 'automatic' },
138
+ });
139
+ ```
140
+
141
+ ### TypeScript JSX Types
142
+
143
+ React TypeScript projects should include the JSX declarations once in a global type file or app entry point:
144
+
145
+ ```tsx
146
+ /// <reference types="fbtee/ReactTypes.d.ts" />
147
+ ```
148
+
149
+ ## Writing Strings
150
+
151
+ Every user-facing string should be wrapped in `<fbt>`, `fbt()`, or `fbs()`. Descriptions are required because they are the translator's context.
152
+
153
+ ```tsx
154
+ <fbt desc="Empty state title when a project has no tasks">No tasks yet</fbt>
155
+ ```
156
+
157
+ Use `fbt()` outside JSX:
158
+
159
+ ```tsx
160
+ import { fbt } from 'fbtee';
161
+
162
+ const title = fbt(
163
+ 'No tasks yet',
164
+ 'Empty state title when a project has no tasks',
165
+ );
166
+ ```
167
+
168
+ Use `fbs()` when you need a plain string:
169
+
170
+ ```tsx
171
+ import { fbs } from 'fbtee';
172
+
173
+ <input
174
+ placeholder={fbs('Search projects', 'Project search input placeholder')}
175
+ />;
176
+ ```
177
+
178
+ ## Dynamic Content
179
+
180
+ Use `<fbt:param>` for dynamic values. Token names should describe the value, not its current English position.
181
+
182
+ ```tsx
183
+ <fbt desc="Greeting with the viewer name">
184
+ Hello, <fbt:param name="viewerName">{viewer.name}</fbt:param>!
185
+ </fbt>
186
+ ```
187
+
188
+ React elements inside `<fbt>` are automatically turned into implicit params:
189
+
190
+ ```tsx
191
+ <fbt desc="Greeting with a linked viewer name">
192
+ Hello, <UserLink user={viewer} />!
193
+ </fbt>
194
+ ```
195
+
196
+ Use `fbt.sameParam()` or `<fbt:same-param>` when the same token appears more than once.
197
+
198
+ ## Plurals
199
+
200
+ Use `<fbt:plural>` when a count controls grammar. _fbtee_ handles locale-specific plural rules.
201
+
202
+ ```tsx
203
+ <fbt desc="Inbox unread count">
204
+ You have{' '}
205
+ <fbt:plural
206
+ count={count}
207
+ many="unread messages"
208
+ name="unreadCount"
209
+ showCount="yes"
210
+ >
211
+ an unread message
212
+ </fbt:plural>
213
+ .
214
+ </fbt>
215
+ ```
216
+
217
+ `showCount` can be `yes`, `ifMany`, or `no`.
218
+
219
+ ## Lists
220
+
221
+ Use `<fbt:list>` for locale-aware lists:
222
+
223
+ ```tsx
224
+ <fbt desc="People assigned to a task">
225
+ Assigned to <fbt:list items={assignees} name="assigneeList" />.
226
+ </fbt>
227
+ ```
228
+
229
+ The standalone `list()` helper is available for non-React code:
230
+
231
+ ```tsx
232
+ import { list } from 'fbtee';
233
+
234
+ const names = list(['Alice', 'Bob', 'Charlie'], 'or', 'comma');
235
+ ```
236
+
237
+ ## Enums
238
+
239
+ Use enums when runtime values map to a fixed set of translatable labels:
240
+
241
+ ```tsx
242
+ const StatusLabels = {
243
+ done: 'done',
244
+ open: 'open',
245
+ };
246
+
247
+ <fbt desc="Task status label">
248
+ This task is <fbt:enum enum-range={StatusLabels} value={status} />.
249
+ </fbt>;
250
+ ```
251
+
252
+ For shared enum modules, use the `$FbtEnum` suffix so the collector can resolve them.
253
+
254
+ ## Pronouns and Gender
255
+
256
+ Use `<fbt:pronoun>` when a phrase depends on a person's gender:
257
+
258
+ ```tsx
259
+ <fbt desc="Photo sharing notification">
260
+ <fbt:param name="name">{user.name}</fbt:param> shared{' '}
261
+ <fbt:pronoun gender={user.gender} human type="possessive" /> photo.
262
+ </fbt>
263
+ ```
264
+
265
+ Supported pronoun types are `subject`, `object`, `possessive`, and `reflexive`.
266
+
267
+ ## Common Strings
268
+
269
+ Common strings are reusable source strings with shared descriptions:
270
+
271
+ ```tsx
272
+ <fbt common>Save</fbt>
273
+ ```
274
+
275
+ Pass common strings to the compiler as `fbtCommon`:
276
+
277
+ ```js
278
+ {
279
+ fbtCommon: {
280
+ Save: 'Button label for saving changes',
281
+ },
282
+ }
283
+ ```
284
+
285
+ ## Runtime Setup
286
+
287
+ Most React apps should use `createLocaleContext`:
288
+
289
+ ```tsx
290
+ import { createLocaleContext } from 'fbtee';
291
+
292
+ const availableLanguages = new Map([
293
+ ['en-US', 'English'],
294
+ ['de-DE', 'Deutsch'],
295
+ ['ja-JP', '日本語'],
296
+ ]);
297
+
298
+ const loadLocale = async (locale: string) => {
299
+ switch (locale) {
300
+ case 'de-DE':
301
+ return (await import('./translations/de-DE.json')).default['de-DE'];
302
+ case 'ja-JP':
303
+ return (await import('./translations/ja-JP.json')).default['ja-JP'];
304
+ default:
305
+ return {};
306
+ }
307
+ };
308
+
309
+ const LocaleContext = createLocaleContext({
310
+ availableLanguages,
311
+ clientLocales: [navigator.language, ...navigator.languages],
312
+ loadLocale,
313
+ });
314
+
315
+ export const Root = () => (
316
+ <LocaleContext>
317
+ <App />
318
+ </LocaleContext>
319
+ );
320
+ ```
321
+
322
+ Use `useLocaleContext` to read or change the locale:
323
+
324
+ ```tsx
325
+ import { useLocaleContext } from 'fbtee';
326
+
327
+ const LanguageButton = () => {
328
+ const { locale, setLocale } = useLocaleContext();
329
+ return <button onClick={() => setLocale('de-DE')}>{locale}</button>;
330
+ };
331
+ ```
332
+
333
+ If you need full control, use `setupLocaleContext` or `setupFbtee` directly.
334
+
335
+ ### Next.js App Router
336
+
337
+ 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.
338
+
339
+ See the [Next.js fbtee example](https://github.com/cpojer/nextjs-fbtee-example) for a complete setup.
340
+
341
+ ## Translation Workflow
342
+
343
+ Extract source strings:
344
+
345
+ ```bash
346
+ pnpm fbtee collect
347
+ ```
348
+
349
+ This writes `source_strings.json`.
350
+
351
+ Prepare editable translation files:
352
+
353
+ ```bash
354
+ pnpm fbtee prepare-translations --source-strings source_strings.json --output-dir translations --locales de-DE fr-FR ja-JP
355
+ ```
356
+
357
+ `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`.
358
+
359
+ Compile translations for the app:
360
+
361
+ ```bash
362
+ pnpm fbtee translate --source-strings source_strings.json --translations 'translations/*.json' --output-dir src/translations
363
+ ```
364
+
365
+ 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`.
366
+
367
+ To migrate editable translation files and generated runtime files to BCP 47 names:
368
+
369
+ ```bash
370
+ pnpm fbtee migrate-locales --to bcp47 --dir translations --dir src/translations
371
+ pnpm fbtee translate --output-locale-style=bcp47
372
+ ```
373
+
374
+ Use `--dry-run` first to preview file renames.
375
+
376
+ Commit the human-authored translation files. Ignore generated runtime output:
377
+
378
+ ```gitignore
379
+ .enum_manifest.json
380
+ source_strings.json
381
+ src/translations/
382
+ ```
383
+
384
+ ## Translating Strings with Coding Agents
385
+
386
+ Coding agents are great at updating _fbtee_ translation files because all the context is in the repository:
387
+
388
+ 1. `fbtee prepare-translations` adds every missing translation and marks it with `"status": "new"`.
389
+ 1. The agent edits only entries with `"status": "new"`.
390
+ 1. The agent removes `"status": "new"` after the translation is complete.
391
+ 1. Code review shows a clean diff of the new localized strings.
392
+
393
+ 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.
394
+
395
+ ### Coding Agent prompt:
396
+
397
+ ```md
398
+ 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.
399
+
400
+ 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.
401
+
402
+ Remove `"status": "new"` from each completed translation entry.
403
+ ```
404
+
405
+ ## ESLint
406
+
407
+ Install the optional ESLint plugin:
408
+
409
+ ```bash
410
+ npm install -D @nkzw/eslint-plugin-fbtee
411
+ ```
412
+
413
+ Use the recommended config:
414
+
415
+ ```js
416
+ import fbtee from '@nkzw/eslint-plugin-fbtee';
417
+
418
+ export default [fbtee.configs.recommended];
419
+ ```
420
+
421
+ Use the strict config if you want every user-facing string to be wrapped:
422
+
423
+ ```js
424
+ import fbtee from '@nkzw/eslint-plugin-fbtee';
425
+
426
+ export default [fbtee.configs.strict];
427
+ ```
428
+
429
+ ## Migration from fbt
430
+
431
+ _fbtee_ is compatible with the core `fbt` programming model:
432
+
433
+ 1. Replace `fbt` packages with `fbtee` and the matching compiler package.
434
+ 1. Replace imports from `fbt` with imports from `fbtee`.
435
+ 1. Use `fbtee collect`, `fbtee prepare-translations`, and `fbtee translate`.
436
+ 1. Convert CommonJS common strings or enum modules to ESM when needed.
437
+ 1. Replace legacy setup calls with `setupFbtee`, `createLocaleContext`, or `setupLocaleContext`.
438
+
439
+ Some archived `fbt` options and legacy behaviors were intentionally removed. The compiler errors should point to the modern replacement when one exists.
440
+
441
+ ## Examples
442
+
443
+ - [Example App](https://github.com/nkzw-tech/fbtee/tree/main/example)
444
+ - [Next.js App Router Example](https://github.com/cpojer/nextjs-fbtee-example)
445
+ - [Athena Crisis](https://github.com/nkzw-tech/athena-crisis)
446
+
447
+ ## Credits
448
+
449
+ - `fbt` was originally created by [Facebook](https://github.com/facebook/fbt).
450
+ - The auto-import plugin was created by [@alexandernanberg](https://github.com/alexandernanberg).
451
+ - [Nakazawa Tech](https://nkzw.tech) rewrote `fbt` into _fbtee_ and maintains this project.
package/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { TransformOptions } from '@nkzw/oxc-transform-fbtee';
2
+ import type { Plugin } from 'vite';
3
+
4
+ export type FbteeVitePluginOptions = Omit<
5
+ TransformOptions,
6
+ 'lang' | 'sourcemap' | 'sourceType'
7
+ >;
8
+
9
+ export declare const fbtee: (options?: FbteeVitePluginOptions) => Plugin;
10
+
11
+ export default fbtee;
package/index.js ADDED
@@ -0,0 +1,53 @@
1
+ import { transformSync } from '@nkzw/oxc-transform-fbtee';
2
+
3
+ const fbteeSourcePattern = /fbt|fbs/;
4
+
5
+ const getLanguage = (id) => {
6
+ const filename = id.split(/[#?]/, 1)[0];
7
+ if (filename.endsWith('.tsx')) {
8
+ return 'tsx';
9
+ }
10
+ if (filename.endsWith('.jsx')) {
11
+ return 'jsx';
12
+ }
13
+ if (filename.endsWith('.ts')) {
14
+ return 'ts';
15
+ }
16
+ return 'js';
17
+ };
18
+
19
+ export const fbtee = (options = {}) => ({
20
+ enforce: 'pre',
21
+ name: 'vite-plugin-fbtee',
22
+ transform: {
23
+ filter: {
24
+ code: fbteeSourcePattern,
25
+ id: {
26
+ exclude: '**/node_modules/**',
27
+ include: '**/*.{js,jsx,ts,tsx}',
28
+ },
29
+ },
30
+ handler(source, id) {
31
+ // Vite and Rolldown apply the hook filter before invoking this handler.
32
+ // Keep the string check as a cheap fallback for compatible hosts that do
33
+ // not yet support hook filters. False positives, including comments, are
34
+ // harmless; every supported fbtee construct contains `fbt` or `fbs`.
35
+ if (!source.includes('fbt') && !source.includes('fbs')) {
36
+ return null;
37
+ }
38
+
39
+ const result = transformSync(id, source, {
40
+ ...options,
41
+ lang: getLanguage(id),
42
+ sourcemap: true,
43
+ sourceType: 'module',
44
+ });
45
+ if (result.errors.length > 0) {
46
+ throw new Error(result.errors.map(({ message }) => message).join('\n'));
47
+ }
48
+ return { code: result.code, map: result.map };
49
+ },
50
+ },
51
+ });
52
+
53
+ export default fbtee;
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@nkzw/vite-plugin-fbtee",
3
+ "version": "3.1.0",
4
+ "description": "Vite plugin for fbtee powered by the native Oxc transform.",
5
+ "keywords": [
6
+ "fbt",
7
+ "fbtee",
8
+ "i18n",
9
+ "oxc",
10
+ "vite"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/nkzw-tech/fbtee.git",
15
+ "directory": "packages/vite-plugin-fbtee"
16
+ },
17
+ "license": "MIT",
18
+ "sideEffects": false,
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./index.d.ts",
23
+ "import": "./index.js",
24
+ "default": "./index.js"
25
+ }
26
+ },
27
+ "main": "index.js",
28
+ "types": "index.d.ts",
29
+ "files": [
30
+ "LICENSE",
31
+ "README.md",
32
+ "index.d.ts",
33
+ "index.js"
34
+ ],
35
+ "dependencies": {
36
+ "@nkzw/oxc-transform-fbtee": "3.1.0"
37
+ },
38
+ "devDependencies": {
39
+ "vite": "^8.1.5"
40
+ },
41
+ "peerDependencies": {
42
+ "vite": ">=6.3.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=22.12.0"
46
+ },
47
+ "scripts": {
48
+ "test": "node test/e2e.mjs"
49
+ }
50
+ }