DEV Community

Cover image for How to add support Typescript for FBT an internationalization framework
Davyd NRB
Davyd NRB

Posted on • Edited on

How to add support Typescript for FBT an internationalization framework

FBT an internationalization framework is a really powerful tool that changes the i18n game!

But when I try using it in Typescript I faced problems. And to answer a question in the title of an article you need to know about limitations:

Hasfbt module a lib definition?

Let's try to use it in the project:

import * as React from "react";
import fbt from "fbt";

const App = () => <fbt desc="welcome message">Hi fbt & Typescript</fbt>;
Enter fullscreen mode Exit fullscreen mode

and you will see the next errors

TS2339: Property 'fbt' does not exist on type 'JSX.IntrinsicElements'.

TS7016: Could not find a declaration file for module 'fbt'. 
  'node_modules/fbt/lib/FbtPublic.js' implicitly has an 'any' type.
  Try `npm install @types/fbt` if it exists or add a new declaration 
  (.d.ts) file containing `declare module 'fbt';`
Enter fullscreen mode Exit fullscreen mode

Let's try to add lib definition using npm module @types/fbt:

yarn add @types/fbt

[1/4] Resolving packages...

error An unexpected error occurred: "https://registry.yarnpkg.com/@types%2ffbt: Not found".
Enter fullscreen mode Exit fullscreen mode

"Not found" it is classic)

1st limitation: As you see we need to create our own lib definition for fbt module & extends JSX.IntrinsicElements interface for support <fbt/> tag.


Does Typescript support XML namespaces syntax <ftb:param>{...}</ftb:param>?

Then let's add param declaration to find out it:

import * as React from "react";
import fbt from "fbt";

const App = () => (
  <fbt desc="welcome message">
    Hi fbt & Typescript<fbt:param name="version">{"3.9.2"}</fbt:param>!
  </fbt>
);
Enter fullscreen mode Exit fullscreen mode

And you will see lots of errors:

Error:(6, 28) TS1003: Identifier expected.
Error:(6, 64) TS1005: '>' expected.
Error:(6, 70) TS1005: ',' expected.
Error:(7,  3)  TS1109: Expression expected.
Error:(8,  1)  TS1109: Expression expected.
Enter fullscreen mode Exit fullscreen mode

This known issue that still opened: https://github.com/microsoft/TypeScript/issues/11833

2nd limitation: Typescript doesn't support XML namespaces syntax


Can we overcome this limitation?

1) First, need to solve 2nd limitation:

You can use undocumented aliases for all helpers:

<fbt:enum/>        <FbtEnum/>
<fbt:param/>       <FbtParam/>
<fbt:plural/>      <FbtPlural/>
<fbt:pronoun/>     <FbtPronoun/>
<fbt:name/>        <FbtName/>
<fbt:same-param/>  <FbtSameParam/>
Enter fullscreen mode Exit fullscreen mode

See: babel-plugin-fbt/FbtUtil.js#L91-L100

2) Then solve 1st limitation:

2.1) Update compilerOptions.typeRoots to use own definitions:

// tsconfig.json
{
  "compilerOptions": {
+    "typeRoots": ["./@types", "./node_modules/@types"]
  }
}
Enter fullscreen mode Exit fullscreen mode

2.2) And create two files:

./@types/fbt/index.d.ts
./@types/fbt/globals.d.ts

These steps enough to Typescript start to understand "fbt syntax"


It should work, right?

NO!!! @babel/preset-typescript has some unclear behavior(

To understand the problem,
I compiled code from the first example using babel repl + @babel/preset-react

// Before
import * as React from "react";
import fbt from "fbt";

const App = () => <fbt desc="welcome message">Hi fbt & Typescript</fbt>;
Enter fullscreen mode Exit fullscreen mode
// After
import * as React from "react";
import fbt from "fbt";

const App = () =>
  React.createElement(
    "fbt",
    { desc: "welcome message" },
    "Hi fbt & Typescript"
  );

Enter fullscreen mode Exit fullscreen mode

<fbt/> => React.createElement("fbt")

As you see above, a fbt variable in import declaration import fbt from "fbt" never used!

Then let's have a look how @babel/preset-typescript works with type imports:

babel

The main idea that @babel/preset-typescript remove unused imports

And when you combine @babel/preset-typescript + babel-plugin-fbt you will be faced with next the error when you try to compile code:

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

# or

error index.js: ./index.js: generateFormattedCodeFromAST is not a function. Run CLI with --verbose flag for more details.
TypeError: ./index.js: generateFormattedCodeFromAST is not a function
    at errorAt (./node_modules/babel-plugin-fbt/FbtUtil.js:237:21)
    at FbtFunctionCallProcessor._assertJSModuleWasAlreadyRequired (./node_modules/babel-plugin-fbt/babel-processors/FbtFunctionCallProcessor.js:158:13)
    at FbtFunctionCallProcessor.convertToFbtRuntimeCall (./node_modules/babel-plugin-fbt/babel-processors/FbtFunctionCallProcessor.js:570:10)
    at PluginPass.CallExpression (./node_modules/babel-plugin-fbt/index.js:188:18)
Enter fullscreen mode Exit fullscreen mode

So what happened?

1) <fbt/> => React.createElement("fbt")
2) after that @babel/preset-typescript see that import fbt from "fbt" never used and remove it
3) then babel-plugin-fbt couldn't find fbt and throw an error

To prevent removing fbt import you need to patch one file node_modules/@babel/plugin-transform-typescript/lib/index.js

  function isImportTypeOnly({
    binding,
    programPath,
    jsxPragma
  }) {
    for (const path of binding.referencePaths) {
      if (!isInType(path)) {
        return false;
      }
    }

+    // Small fix to stop removing `import fbt from 'fbt';`
+    if (binding.identifier.name === 'fbt') {
+      return false;
+    }


    if (binding.identifier.name !== jsxPragma) {
      return true;
    }
Enter fullscreen mode Exit fullscreen mode

How to apply patch after install?
Use postinstall script:

{
  "scripts": {
    "postinstall": "node path.js",
  }
}
Enter fullscreen mode Exit fullscreen mode
// path.js
const { readFileSync, writeFileSync } = require('fs');

const patch = `
    // Small fix to stop removing \`import fbt from 'fbt';\`
    if (binding.identifier.name === 'fbt') {
      return false;
    }
`;

const FILE_PATH = require.resolve(
  '@babel/plugin-transform-typescript/lib/index.js',
);

const data = readFileSync(FILE_PATH).toString();
const isAlreadyPatched = data.includes("binding.identifier.name === 'fbt'");

if (isAlreadyPatched) {
  process.exit(0);
}

writeFileSync(
  FILE_PATH,
  data.replace(
    'if (binding.identifier.name !== jsxPragma) {',
    `${patch}\nif (binding.identifier.name !== jsxPragma) {`,
  ),
);
Enter fullscreen mode Exit fullscreen mode

Утиииии We made it!
So that enough)
if you have any question I am glad to discuss them in the comments!

(c) MurAmur

Top comments (8)

Collapse
 
yrichard profile image
Yohann Richard

Hey David, this is all so great, you rock! Can you make an npm package with those type definition files? Or I can do it, or we can do it as a shared project, I don't really mind as long as it's done, haha :) Please reach out to me, I'd love to get this out. Thanks.

Collapse
 
retyui profile image
Davyd NRB

I have nothing if you make a @types/fbt module!

Collapse
 
yrichard profile image
Yohann Richard

hmm, I suppose you mean "you have nothing against me making @types/fbt", right? (that's ok, english wasn't my first language either) I will get on it right away. I will add you as a contributor as well. And, btw, I will tell my buddies at Facebook they should interview you for a job ;)

Collapse
 
lpaydat profile image
Prathak Paisanwatcharakit

Great work!! David. Thank you very much for your guide.

However, I have faced another issue recently, with next@9.5.3, and maybe next-transpile-modules@^4.1.0, I got fbt is not defined on the server (it still works on localhost but not on the server).

This issue is gone if I downgrade next and next-transpile-modules to 9.3.5 and 3.1.0.
By the error message, I guess the fbt was removed somewhere by some library.

Collapse
 
khadorkin profile image
Dzmitry Khadorkin

Seems like FbtParam or FbtPlural is not works in React Native

Collapse
 
retyui profile image
Davyd NRB

What exactly "not works"
How can I reproduce it?

Collapse
 
khadorkin profile image
Dzmitry Khadorkin • Edited

I just cloned your repo github.com/retyui/horoscope and changed github.com/retyui/horoscope/blob/m...
<fbt>...</fbt>
to <FbtPlural count={5}>...</FbtPlural> - as the result - FbtPlural is undefined. Of course I imported it from fbt. Looks like metro-config breaks babel-plugin-fbt

Thread Thread
 
retyui profile image
Davyd NRB • Edited

Sorry but this project didn't create as a demo to fbt

Anyway, I added an example of plural:
github.com/retyui/horoscope/commit...

And if it works!