Gesti贸n de literales en react con react-intl

Tiempo de lectura: 6 min

En esta publicaci贸n, vamos a adentrarnos en un enfoque para la gesti贸n de literales de un proyecto de React utilizando react-intl.

Configurando un nuevo proyecto con create-react-app

Iniciamos un nuevo proyecto de React con CRA, una de las formas m谩s f谩ciles de hacerlo sin tener que lidiar con la configuraci贸n de webpack.

npx create-react-app react-intl-example --template typescript
cd react-intl-example
npm start

Instalando react-intl

El siguiente paso es instalar react-intl como una dependencia:

npm i react-intl

Tambi茅n necesitaremos una dependencia de desarrollo para extraer y compilar los literales:

npm i -D @formatjs/cli

Usando react-intl en una aplicaci贸n de React

Comencemos a usar react-intl en nuestra aplicaci贸n de React de prueba.

Crea una carpeta lang en src con un archivo JSON vac铆o llamado en.json. Este archivo contendr谩 m谩s tarde el resultado de la compilaci贸n para cada literal en la aplicaci贸n. Explicaremos m谩s adelante en detalle c贸mo lograr esto.

{}

Editamos index.tsx en la carpeta src. Tenemos que importar IntlProvider de react-intl y envolver nuestra aplicaci贸n con 茅l. Estamos creando algunos componentes para rich elements que por defecto se usar谩n globalmente en los literales de la aplicaci贸n.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { IntlProvider } from "react-intl";
import literals from "./lang/en.json";

interface Props {
  children?: React.ReactNode;
}

const Bold = (props: Props) => {
  return (
    <div>
      <strong>{props.children}</strong>
    </div>
  );
};

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <IntlProvider
    locale="en"
    defaultLocale="en"
    messages={literals}
    defaultRichTextElements={{
      bold: (chunks) => <Bold>{chunks}</Bold>,
    }}
  >
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </IntlProvider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

El siguiente paso es crear una nueva carpeta con un archivo example.ts donde definiremos algunos literales. Por ejemplo, uno usando un rich element global definido anteriormente (usado como una etiqueta HTML, <bold></bold> en este ejemplo), y otro literal regular simple. Lo haremos utilizando la funci贸n defineMessages de react-intl.

import { defineMessages } from "react-intl";

export default defineMessages({
  hello: {
    id: "a.hello",
    defaultMessage: "<bold>hello</bold>",
  },
  world: {
    id: "a.world",
    defaultMessage: "world",
  },
});

Podemos crear otro archivo de literales llamado other.ts dentro de la carpeta messages. Esto es solo un ejemplo, puedes crear tantos archivos de literales como desees y colocarlos donde quieras, esa decisi贸n est谩 en tus manos y es subjetiva.

import { defineMessages } from "react-intl";

export default defineMessages({
  other: {
    id: "a.richtext",
    defaultMessage: "I have <test>{num}</test>",
  },
});

Ahora podemos comenzar a importar nuestros literales en la aplicaci贸n. Para simplificar, mostrar茅 c贸mo hacerlo editando directamente el archivo App.tsx de la siguiente manera:

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;

En este ejemplo, estamos cargando nuestros literales desde dos archivos diferentes y utilizando el componente FormattedMessage de la biblioteca react-intl. Puedes consultar en la documentaci贸n oficial diferentes formas de declarar mensajes.

Por lo tanto, cada vez que deseemos usar un literal en la aplicaci贸n, podemos definirlo en un archivo separado e importarlo para usarlo. Si no se encuentra un literal en el archivo compilado lang/en.json, se utilizar谩 el valor proporcionado en defaultMessage. Esto es muy 煤til porque no es necesario compilar los literales cada vez que necesitemos un nuevo literal mientras desarrollamos.

Extrayendo y compilando literales con formatJS

Crearemos algunos scripts en el archivo package.json para automatizar el proceso de extracci贸n y compilaci贸n de literales. Podemos consultar la documentaci贸n oficial para obtener m谩s detalles. Utilizaremos un comando muy largo de la documentaci贸n y lo dividiremos en algunos scripts para una mejor legibilidad. Veamos los scripts y luego la explicaci贸n de los comandos ejecutados:

// ---- 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",
}
// ----

El primer comando extraer谩 cada una de los literales definidos en la aplicaci贸n bajo la carpeta src a un archivo temporal llamado temp.json.

formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file temp.json --flatten --id-interpolation-pattern '[sha512:contenthash:base64:6]'

Advertencias: El comando incluye una opci贸n para generar ids, pero con la configuraci贸n de create-react-app, esta caracter铆stica no funcionar谩 porque es necesario editar la configuraci贸n de webpack y babel.

Una vez extra铆dos los literales a un archivo, los compilaremos especificando el archivo de destino (destacar los par谩metros adicionales utilizados en el comando, no est谩n presentes en el script anterior, agregaremos m谩s scripts al final):

formatjs compile 'temp.json' --out-file src/lang/en.json

Con este comando, el archivo generado (src/lang/en.json) ser谩 el siguiente:

{
  "a.hello": "<bold>hello</bold>",
  "a.richtext": "I have <test>{num}</test>",
  "a.world": "world"
}

Si deseamos traducir nuestros literales a otro idioma, este archivo debe ser utilizado como punto de partida, y se deben traducir cada uno de los literales. Luego, debemos crear un nuevo archivo, por ejemplo es.json, y agregar l贸gica en index.tsx para cargar el archivo en.json o es.json dependiendo del idioma seleccionado en la aplicaci贸n.

Si iniciamos nuestra aplicaci贸n con este archivo, todo funcionar谩 seg煤n lo esperado, pero en las DevTools se puede leer esta advertencia:

[@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.

Esto se debe a que estamos usando la opci贸n defaultRichTextElements globalmente para los literales, y cada vez que se carga un literal, la biblioteca no sabe si el literal est谩 utilizando defaultRichTextElements o no. Por esta raz贸n, debemos compilar utilizando el flag ast:

formatjs compile 'temp.json' --ast --out-file src/lang/en.json

Despu茅s de esto, el resultado ser谩 diferente:

{
  "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"
    }
  ]
}

Y si iniciamos la aplicaci贸n con este archivo, la advertencia desaparecer谩. Finalmente, podemos agregar m谩s scripts con los flags necesarios para cada caso. Extracto final de los 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"
}
// ----

Los 煤ltimos 2 scripts est谩n utilizando npm-run-all como una dependencia de desarrollo, puedes instalarla usando: npm i -D npm-run-all.

Para generar los literales en la aplicaci贸n, podemos ejecutar literals:extract:compile para generar un archivo listo para ser traducido, o literals:extract:compile:ast para un archivo listo para producci贸n.

Se puede revisar un repositorio con este ejemplo de aplicaci贸n en mi cuenta de Github.

Nota: No he probado BalbelEdit pero parece que puede ser muy 煤til a la hora de traducir una aplicaci贸n, soporta React.