Configuring FBT API with TypeScript and create-react-app.

Yohann Richard
8 min readMay 20, 2020

--

The default React App example page fully translated to french.

UPDATE 05/28/2020: I have published a npm package taking care of the deployment for you. https://www.npmjs.com/package/fbt-cra-typescript-installer

Let me take you on the adventure of a lifetime, dear reader. It will be tough, there will be tears, but the reward is so sweet, trust me, you will want to see this through.

“So you want to do some I18N like a boss?”, said the booming voice. You look around, but there’s nobody. The voice is in your head. You have the drive, you have the motivation to be the best, so there is no other choice. You NEED to use the FBT API to translate the strings of your React application.

Why You Should Do This To Yourself.

The FBT API has been used at Facebook for many years to handle everything, from very simple strings, to extremely complex constructs with genders, plurality, enumerations, pronouns etc… And the best thing? It’s dead simple to use. I know because I worked there for ten years and, even though I’m limited, I was able to use it.

Consider translating this:

<div>
Go on an <a href="#"><span>awesome</span> vacation</a>
</div>

You might go about splitting this into chunks, wrapping each piece of string within some function that would fetch each translated chunk, or you might just go and rewrite the block of code for each language you want to support. This is less than ideal.

Here’s how you do it with FBT:

<div>
<fbt desc="some example">
Go on an <a href="#"><span>awesome</span> vacation</a>
</fbt>
</div>

And that’s it, you can move on with your day, feeling complete, and focusing on stuff that really matters.

“OMG! I WANT THIS THING IN MY LIFE!” — You

I know you do, this is pure gold.

But, as in reality, treasures don’t come easily. You need to fight for them.

Installing CRA + TypeScript

Just in case you haven’t done this already, let’s create a React app with TypeScript enabled.

For this, we’ll use the recommended way, “create-react-app” aka CRA.

npx create-react-app my-fbt-app --template typescript
cd my-fbt-app
yarn start

You should now see the sweet spinning React logo in your browser. If you don’t, go check the documentation. We’ll wait for you here, good luck.

Most probably, though, it worked like a charm. It was easy, right? That’s because CRA has hidden all the hard configuration options away from you, including things like “Babel” and “Webpack” options. If you don’t know what those two are, I won’t explain in detail here, but let’s just say that “Babel” is something that can read, transform and validate code (for example, there’s a Babel transform translating TypeScript into Javascript), and Webpack is something to package your code, manage build tools, etc…

Now you may head over the FBT API documentation, and read it a bit. At this point in time (May 19th, 2020 AD, or year 0 After Coronavirus), the raw charm of the documentation might make your head spin a little.

Let’s start with the easy bits and install a few packages.

yarn add fbt
yarn add babel-plugin-fbt
yarn add babel-plugin-fbt-runtime

What are all those? Don’t worry about it too much, it’s complicated. But in short the Babel plugins will read the code to locate and extract the strings to translate, and the “fbt” module will be loaded in the browser to actually render the strings.

Now the documentation talks about how you need to configure Babel with those plugins, but careful! You need to do it in a particular order. Oh, and there’s this webpack example, that you may or may not need to look at, it’s unclear. You may feel worried. It’s normal. After all, “create-react-app” has hidden all of this from you, so that you could focus on coding. There are ways to expose those hidden configuration files, by running “yarn eject”, but don’t do it, it’s just more headaches.

Setting Up Craco

Instead of getting crazy with CRA’s internal configurations, let’s introduce Craco, which means “CRA CO-nfiguration”. Craco will let you add Babel and Webpack configurations to your CRA deployment somewhat easily.

Let’s install it:

yarn add @craco/craco

Then open the file package.json in the app root directory, and look for these:

"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},

Then replace the ‘react-scripts’ with ‘craco’ for the scripts ‘start’, ‘build’ and ‘test’:

"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

Finally, let’s add a file craco.config.js next to package.json, with the following content:

module.exports = {};

Run “yarn start”, just for good measure, to make sure your app is still running.

Configuring FBT

Now we’re going to actually configure FBT to run within your app, so hang on tight.

First, we’ll add the plugins to the craco.config.js:

module.exports = {
babel: {
plugins: [
[
"babel-plugin-fbt",
{
fbtEnumManifest: require("./.enum_manifest.json"),
extraOptions: { __self: true },
},
],
"babel-plugin-fbt-runtime",
],
},
};

Then we’ll add those scripts to package.json:

scripts: {
...,
"manifest": "babel-node node_modules/.bin/fbt-manifest --src src", "collect-fbts": "babel-node node_modules/.bin/fbt-collect --pretty --manifest < .src_manifest.json > .source_strings.json", "test-collect-fbts": "babel-node node_modules/.bin/fbt-collect --plugins @babel/plugin-syntax-flow --pretty --manifest < .src_manifest.json > .test_source_strings.json", "translate-fbts": "babel-node node_modules/.bin/fbt-translate --translations translations/*.json --jenkins > src/translatedFbts.json",},

If you’re paying attention, you might have noticed the calls to ‘babel-node’. Let’s install that darn thing.

yarn add -D @babel/core @babel/node

Then let’s try this command to see if it works:

yarn manifest

I will explain what this command is about later. For now, verify that two files have been created: .enum_manifest.json and .src_manifest.json.

.enum_manifest.json:

{}

.src_manifest.json:

{".enum_manifest.json":[""]}

Adding FBT To Your Code

Now let’s edit the file src/App.tsx:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import fbt from 'fbt';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
<fbt desc="example">Learn React</fbt>
</a>
</header>
</div>
);
}
export default App;

At this point, your text editor might highlight several problems.

Could not find a declaration file for module ‘fbt’.

and:

Property ‘fbt’ does not exist on type ‘JSX.IntrinsicElements’.

The first one is because the module “fbt” isn’t typed, so TypeScript can’t know what types this module returns. Usually, you can run a command to download the type definition for an imported module:

yarn add -D @types/fbt

But as of today, this will not work, because we’re still waiting for a kind soul to actually create that type definition. This could be you! Be a hero.

The second error (Property ‘fbt’ does not exist on type ‘JSX.IntrinsicElements’.) is because TypeScript doesn’t recognize <fbt /> as being a proper React element.

So, until you get to save us all, we’re going to do the next best thing, and add a file src/decs.d.ts with the following content to address those two issues:

declare module "fbt";declare namespace JSX {  
interface IntrinsicElements {
fbt: any;
}
}

You might still see something saying:

‘fbt’ is declared but its value is never read.

We’ll address this one later, because it’s nasty.

Running The Scripts

Let’s look at those scripts we’ve added:

  • yarn manifest: Scans provided filesystem paths and generates a manifest of the enumeration modules. This is advanced magic, I won’t explain it right now.
  • yarn collect-fbts: Given source input, extract any source text and print them to STDOUT as JSON. In our case, we’ve redirected the output to a file .source_strings.json.
  • yarn translate-fbts: Creates translation payloads for runtime by taking extracted source text (from collectFBT) and translations provided in JSON format to produce these payloads.

Run the first two commands:

yarn manifest
yarn collect-fbts

You should see those files:

.src_manifest.json:

{".enum_manifest.json":["src/App.tsx","src/decs.d.ts"]}

.source_strings.json:

{
"phrases": [
{
"hashToText": {
"RqH6/1RO1wC8xRjKt95P5A==": "Learn React"
},
"filepath": "src/App.tsx",
"line_beg": 21,
"col_beg": 10,
"line_end": 21,
"col_end": 47,
"type": "text",
"desc": "example",
"project": "",
"jsfbt": "Learn React"
}
],
"childParentMappings": {}
}

So far, so good! Let’s translate this into French.

Our First Translation

Create a directory ‘translations’ and the file translations/fr_FR.json with the following content:

{
"fb-locale": "fr_FR",
"translations": {
"RqH6/1RO1wC8xRjKt95P5A==": {
"tokens": [],
"types": [],
"translations": [
{
"translation": "Apprendre React",
"variations": {}
}
]
}
}
}

If you squint your eyes and tilt your head to the right, you will notice that the string “RqH6/1RO1wC8xRjKt95P5A==” is common to those two files. I let you guess why that is. If you have a different string, just make sure the one in fr_FR.json matches the one in .source_strings.json.

You might think that this file fr_FR.json is a pain to write, but hey, that shouldn’t be your problem. That’s why you got a team of professional translators for. They will deal with it, right? RIGHT?

Next, we’ll run this command:

yarn translate-fbts

Then check that a file src/translatedFbts.json has been created with the following content:

{"fr_FR":{"33rj7N":"Apprendre React"}}

Finally, let’s reopen src/App.tsx and add a few lines:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import fbt, { IntlViewerContext, init } from "fbt";
import intl from "./translatedFbts.json";
// This will load the translated strings in FBT.
init({ translations: intl });
IntlViewerContext.locale = "fr_FR";function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
<fbt desc="example">Learn React</fbt>
</a>
</header>
</div>
);
}
export default App;

Let’s restart the app:

yarn start

The Nasty Bit

It should work, right? Ohh, but no, you are getting this error:

fbt is not bound. Did you forget to require('fbt')?

You might be dumbfounded, I know I was. Unfortunately, I don’t have a good solution for this. I bugged the FBT API team on their Facebook group, and I’ve been told they’re looking into it. For now, you’re gonna do the nasty something I told you about earlier. Just add those lines everywhere you need to import fbt:

import React from 'react';
import logo from './logo.svg';
import './App.css';
import fbt, { IntlViewerContext, init } from "fbt";
// Needed until it get resolved with the FBT team.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
fbt;

Now, go take a shower, this never happened. Then restart the app:

yarn start

And observe:

WE MADE IT! CONGRATS! WEEEEEEHEEEEEE!

Special thanks to John Watson and Daniel Lo Nigro for their hints!

I have shared the code in a repository HERE.

This tutorial is not fully complete, you should be able to get basic translations to work, but for more advanced features, please follow these steps: https://dev.to/retyui/how-to-add-support-typescript-for-fbt-an-internationalization-framework-3lo0. (Thanks Mur Amur!)

Other errors I’ve seen

Invalid option "__self". Only allowed: project, author, preserveWhitespace, subject, common, doNotExtract

Solution: Make sure you added this to your craco.config.json (see full file above).

extraOptions: { __self: true },

As for this error when running yarn collect-fbts:

Unexpected token, expected ";"

I have seen this when I was using the ‘!’ operator to give a hint to TypeScript. For example:

const someVariable =
someOtherVariable.someAttribute!.someOtherThing;

That’s because ‘babel-node’ runs with a different config than CRA and Craco, and doesn’t use the proper TypeScript presets.

Modify your craco.config.js like this:

module.exports = {
babel: {
presets: ["@babel/preset-typescript"],
plugins: [
[
"babel-plugin-fbt",
{
fbtEnumManifest: require("./.enum_manifest.json"),
extraOptions: { __self: true },
},
],
"babel-plugin-fbt-runtime",
],
},
};

Add a babel.config.js:

const cracoConfig = require("./craco.config");
module.exports = function(api) {
api.cache(true);
return cracoConfig.babel;
};

And import the preset:

yarn add -D @babel/preset-typescript

Then send me a thank-you note.

--

--