Table of content
In this post, we are going to dive into an approach for literals management in a React project using react-intl.
Setting up a new project with create-react-app
Let’s initialize a new react project with CRA, one of the easiest ways without having to deal with webpack configuration.
npx create-react-app react-intl-example --template typescript
cd react-intl-example
npm start
Installing react-intl
The next step is installing react-intl
as a dependency:
npm i react-intl
We will also need one dev dependency for extracting and compiling the literals:
npm i -D @formatjs/cli
Using react-intl
in a React app
Let’s start using react-intl
in our dummy React app.
Create a folder lang
at src
with an empty JSON file called en.json
. This file will contain later the result of the compilation for every literal in the app. We will explain later in detail how to achieve this.
{}
Edit index.tsx
in the src
folder. We have to import IntlProvider
from react-intl
and wrap our app with it. We are creating some components for default rich elements that will be used globally in the literals in the app.
|
|
The next step is creating a new folder with a file example.ts
where we will define some literals. For example, one using the global rich element defined early (used like an HTML tag, <bold></bold>
in this example), and one simple regular literal. We will do that using the defineMessages
function from react-intl
.
import { defineMessages } from "react-intl";
export default defineMessages({
hello: {
id: "a.hello",
defaultMessage: "<bold>hello</bold>",
},
world: {
id: "a.world",
defaultMessage: "world",
},
});
We can create another literals file called other.ts
inside messages
folder. This is just an example, you can create as many literals files as you want and put it wherever you want, that decision is up to you and is opinionated.
import { defineMessages } from "react-intl";
export default defineMessages({
other: {
id: "a.richtext",
defaultMessage: "I have <test>{num}</test>",
},
});
Now we can start importing our literals into the app, for simplicity, I will show it editing directly App.tx
file on this way:
import React from "react";
import { FormattedMessage } from "react-intl";
import exampleMessages from "./messages/example";
import otherMessages from "./messages/other";
function App() {
return (
<div className="App">
<FormattedMessage {...exampleMessages.hello} />{" "}
<FormattedMessage {...exampleMessages.world} />
<FormattedMessage
id={otherMessages.other.id}
defaultMessage={otherMessages.other.defaultMessage}
values={{ num: 99, test: (chunks: any) => <strong>{chunks}!!</strong> }}
/>
</div>
);
}
export default App;
In this example, we are loading our literals from two different files and using the FormattedMessage
component from react-intl
library. You can check in the official documentation different ways of declaring messages.
Therefore, whenever you want to use a literal in your app, you can define it in a separate file and import it to use it. If no literal is found at the compiled file lang/en.json
, the value provided in defaultMessage
will be used. This is very useful because it is not necessary to compile literals each time you need a new literal while developing.
Extracting and compiling literals with formatJS
We will create some scripts in the package.json
to automatize the process for extracting and compiling literals. You can take a look at the official documentation for further details. We will use a very long command from the documentation and split it into some scripts for better readability. Let’s see the scripts and thereafter the explanation for the commands executed:
// ---- scripts section from package.json
"scripts": {
"literals:extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file temp.json --flatten --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"literals:compile": "formatjs compile 'temp.json'",
"postliterals:compile": "rm temp.json",
}
// ----
The first command will extract each of the literals defined in the app under src
folder to a temp file called temp.json
.
formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file temp.json --flatten --id-interpolation-pattern '[sha512:contenthash:base64:6]'
Cautions: The command includes an option for generating ids, but with
create-react-app
config, this feature will not work because it is necessary to edit webpack and babel configuration.
Once extracted to a file, we will compile it, specifying the destination file (notice the extra params used in the command, they are not present in the previous script, we will add more scripts at the end):
formatjs compile 'temp.json' --out-file src/lang/en.json
With this command, the file generated (src/lang/en.json
) will be this one:
{
"a.hello": "<bold>hello</bold>",
"a.richtext": "I have <test>{num}</test>",
"a.world": "world"
}
If we wanted to translate our literals to another language, this should be the source file used as starting point, and translating every literal. Then we should add a new file, for example
es.json
and add logic in theindex.tsx
to loaden.json
ores.json
file depending on the language selected in the app.
If we start our application with this file, everything will be working as expected, but in the DevTools you can see this warning:
[@formatjs/intl] "defaultRichTextElements" was specified but "message" was not pre-compiled. Please consider using "@formatjs/cli" to pre-compile your messages for performance. For more details see https://formatjs.io/docs/getting-started/message-distribution
.
This is because we are using the option defaultRichTextElements
globally for literals, and each time a literal is loaded, the library doesn’t know whether the literal is using that defaultRichTextElements
or not. For that reason, we should compile using the ast
flag option:
formatjs compile 'temp.json' --ast --out-file src/lang/en.json
After this, the result file will be different:
{
"a.hello": [
{
"children": [
{
"type": 0,
"value": "hello"
}
],
"type": 8,
"value": "bold"
}
],
"a.richtext": [
{
"type": 0,
"value": "I have "
},
{
"children": [
{
"type": 1,
"value": "num"
}
],
"type": 8,
"value": "test"
}
],
"a.world": [
{
"type": 0,
"value": "world"
}
]
}
And if we start the app with this file, the warning will be gone. Finally, we can add more scripts with the flags necessaries for each case. Final excerpt from scripts:
// ---- scripts section from package.json
"scripts": {
"literals:extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file temp.json --flatten --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
"literals:compile": "formatjs compile 'temp.json'",
"postliterals:compile": "rm temp.json",
"literals:en": "npm run literals:compile -- --out-file src/lang/en.json",
"literals:en:ast": "npm run literals:compile -- --ast --out-file src/lang/en.json",
"literals:extract:compile": "npm-run-all literals:extract literals:en",
"literals:extract:compile:ast": "npm-run-all literals:extract literals:en:ast"
}
// ----
The last 2 scripts are using
npm-run-all
as dev dependency, you can install it using:npm i -D npm-run-all
.
For generating the literals in the app, we can execute literals:extract:compile
for generating a file ready to be translated or literals:extract:compile:ast
for a production-ready file.
You can check a repository with this simple example app in my Github account.
Note: I have not tested BalbelEdit, but it seems it can be very useful to translate an app and it supports React.