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>;
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';`
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".
"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>
);
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.
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/>
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"]
}
}
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>;
// After
import * as React from "react";
import fbt from "fbt";
const App = () =>
React.createElement(
"fbt",
{ desc: "welcome message" },
"Hi fbt & Typescript"
);
<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:
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)
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;
}
How to apply patch after install?
Use postinstall
script:
{
"scripts": {
"postinstall": "node path.js",
}
}
// 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) {`,
),
);
Утиииии 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)
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.
I have nothing if you make a
@types/fbt
module!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 ;)
Great work!! David. Thank you very much for your guide.
However, I have faced another issue recently, with
next@9.5.3
, and maybenext-transpile-modules@^4.1.0
, I gotfbt is not defined
on the server (it still works on localhost but not on the server).This issue is gone if I downgrade
next
andnext-transpile-modules
to9.3.5
and3.1.0
.By the error message, I guess the
fbt
was removed somewhere by some library.Seems like
FbtParam
orFbtPlural
is not works in React NativeWhat exactly "not works"
How can I reproduce it?
I just cloned your repo github.com/retyui/horoscope and changed github.com/retyui/horoscope/blob/m...
to<fbt>...</fbt>
<FbtPlural count={5}>...</FbtPlural>
- as the result -FbtPlural
is undefined. Of course I imported it fromfbt
. Looks likemetro
-config breaksbabel-plugin-fbt
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!