https://d226lax1qjow5r.cloudfront.net/blog/blogposts/real-time-sms-demo-with-react-node-and-google-translate-dr/E_SMS-Translations_1200x600.png

Demostración de SMS en tiempo real con React, Node y Google Translate

Publicado el May 24, 2021

Tiempo de lectura: 8 minutos

El año pasado trabajé con la API de Google Translate para traducir mensajes SMS. Después de enseñárselo al resto del equipo, querían una demo que pudieran mostrar a otros desarrolladores en las conferencias a las que asistíamos. A partir de ahí, me propuse crear un frontend con React que pudiera mostrar las traducciones en tiempo real.

Creación del WebSocket

¿Qué es un WebSocket?

Para esta demostración, decidí que utilizar un WebSocket sería una gran solución. Si no has usado un WebSocket antes, es un protocolo que permite a un cliente y a un servidor comunicarse en tiempo real. Los WebSockets son bidireccionales, lo que significa que tanto el cliente como el servidor pueden enviar y recibir mensajes. Cuando te conectas por primera vez a un WebSocket, la conexión se realiza pasando de un protocolo HTTP al protocolo WebSocket y se mantiene viva mientras no se interrumpa. Una vez establecida, proporciona un flujo continuo de contenido. Exactamente lo que necesitamos para recibir mensajes SMS entrantes y traducidos.

Crear el servidor WebSocket en Node

Como paso inicial para crear los WebSockets, el servidor requiere una ruta para permitir las conexiones del cliente. Comenzando con el archivo original del servidor de mi post anteriorpodemos hacer unos pequeños cambios para crear el servidor WebSocket y los eventos y listeners requeridos por el cliente.

Usando el paquete ws de NPM, podemos crear rápidamente lo que necesitamos para que esto funcione.

npm install ws

Una vez instalado, incluya el paquete en su archivo de servidor y cree el servidor WebSocket. WS permite una path para establecer la ruta que utiliza el cliente para conectarse.

const express = require('express');
const WebSocket = require('ws');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server, path: "/socket" });

Con este trozo de código, el cliente tiene ahora un lugar para conectarse a la ruta WebSocket /socket. Con el servidor listo para funcionar, ahora necesitas escuchar un evento connection evento. Cuando el cliente se conecta, el servidor utiliza lo siguiente para configurar los otros oyentes que necesitamos:

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.translateTo = 'en';

  ws.on('pong', () => {
    ws.isAlive = true;
  });

  ws.on('message', (message) => {
    translateTo = message;
  });

});

Hay dos puntos principales a destacar:

  1. En la conexión, establecemos la propiedad isAlive a truey escuchamos el evento pong evento. Este evento es para que el servidor compruebe y mantenga una conexión con el cliente. El servidor envía un ping y responde con pong para verificar que la conexión sigue activa.

  2. Aquí configuro translateTo como propiedad a almacenar. translateTo se establece a través de cada cliente utilizando un desplegable. Cuando alguien usando nuestra aplicación de demostración de cabina selecciona un idioma diferente, esa acción establece esto para traducir los textos SMS al idioma solicitado.

Mantener viva la conexión

Un punto esencial del que hay que preocuparse es comprobar si hay clientes que se desconectan. Es posible que durante el proceso de desconexión, el servidor no se dé cuenta y se produzcan problemas. Con un buen amigo setInterval()podemos comprobar si nuestros clientes siguen ahí y reconectarlos si es necesario.

setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping(null, false, true);
  });
}, 10000);

Envío de mensajes al cliente

Ahora que el WebSocket está conectado y monitorizado, podemos manejar los mensajes entrantes de Nexmo, la traducción y la respuesta al cliente. El método handleRoute necesita ser actualizado desde su estado original para añadir la respuesta para cada cliente.

const handleRoute = (req, res) => {

  let params = req.body;

  if (req.method === "GET") {
    params = req.query
  }

  if (!params.to || !params.msisdn) {
    res.status(400).send({ 'error': 'This is not a valid inbound SMS message!' });
  } else {
    wss.clients.forEach(async (client) => {
      let translation = await translateText(params, client.translateTo);
      let response = {
        from: obfuscateNumber(req.body.msisdn),
        translation: translation.translatedText,
        originalLanguage: translation.detectedSourceLanguage,
        originalMessage: params.text,
        translatedTo: client.translateTo
      }

      client.send(JSON.stringify(response));
    });

    res.status(200).end();
  }

};

El método wss.clients.forEach itera a través de cada conexión, y envía los parámetros SMS desde Nexmo a la API de Google Translate. Una vez que la traducción vuelve, podemos decidir qué datos debe tener el front-end, y pasarlo de vuelta como una cadena como he hecho aquí con client.send(JSON.stringify(response)).

Para recapitular lo que ha sucedido aquí: Cada cliente se conecta al servidor WebSocket llamando a la ruta /socket y establece una conexión. Un mensaje SMS va desde el teléfono del remitente a Nexmo, que a su vez llama a la ruta /inboundSMS ruta. La aplicación pasa el mensaje de texto a Google Translate API para cada cliente conectado y, finalmente, lo envía de vuelta a la interfaz de usuario del cliente.

Diagram of WebSocket FlowDiagram of WebSocket Flow

A continuación, vamos a construir las partes de interfaz de usuario para mostrarlo en la pantalla.

WebSockets con React

Con el servidor WebSocket en funcionamiento, podemos pasar a la visualización de los mensajes en pantalla. Como me gusta usar Reacty lo que es más importante React Hooksme puse a buscar algo que me ayudara a conectarme a WebSockets. Por supuesto, he encontrado uno que se adapte a mi necesidad exacta.

La interfaz de usuario de la aplicación de demostración está construida con create-react-appy he utilizado el Grommet framework. Estos temas están fuera del alcance de este post, pero usted puede agarrar mi código fuente y seguir adelante.

Conexión al WebSocket

El primer paso es establecer una conexión e iniciar una comunicación bidireccional. El módulo que encontré es react-use-websockete hizo que la configuración fuera muy sencilla.

npm install react-use-websocket

Hay toneladas de estas bibliotecas gancho React por ahí que le ayudan a crear una funcionalidad impresionante en un corto período de tiempo. En este caso, basta con importar el módulo y configurar un par de elementos para obtener una conexión.

import useWebSocket from 'react-use-websocket';

const App = () => {
  const STATIC_OPTIONS = useMemo(() => ({
    shouldReconnect: (closeEvent) => true,
  }), []);

  const protocolPrefix = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
  let { host } = window.location;
  const [sendMessage, lastMessage, readyState] = useWebSocket(`${protocolPrefix}//${host}/socket`, STATIC_OPTIONS);

  //...
}

En el componente, importamos el método useWebSocket para pasar la URL del WebSocket y el objeto STATIC_OPTIONS como segundo argumento. El método useWebSocket es un gancho personalizado que devuelve el método sendMessage del método, lastMessage el objeto del servidor (que son nuestros mensajes traducidos), y el objeto readyState que es un entero que nos da el estado de la conexión.

Recepción de mensajes entrantes

Una vez que react-use-websocket establece la conexión con el servidor, ya podemos empezar a escuchar los mensajes de la propiedad lastMessage propiedad. Cuando se reciben mensajes entrantes del servidor, se rellenan aquí y actualizan el componente. Si tu servidor tiene múltiples tipos de mensajes, discierne esa información aquí. Como nosotros sólo tenemos uno, es una implementación más sencilla.

const [messageHistory, setMessageHistory] = useState([]);

useEffect(() => {
  if (lastMessage !== null) {
    setMessageHistory(prev => prev.concat(lastMessage))
  }
}, [lastMessage]);

return (
  <Main>
    {messageHistory.map((message, idx) => {
      let msg = JSON.parse(message.data);
      return (
        <Box>
          <Text>From: {msg.from}</Text>
          <Heading level={2}>{msg.translation}</Heading>
        </Box>
      )
    })}
  </Main>
)

El gancho incorporado useEffect se ejecuta cada vez que se actualiza el estado. Cuando lastMessage no es nulo, añade el nuevo mensaje al final de la matriz de estado de mensajes anterior, y la interfaz de usuario se actualiza utilizando la función map para mostrar todos los mensajes. Es en el messageHistory donde se almacenan todas las cadenas JSON que pasamos desde el servidor. La funcionalidad principal de nuestro WebSocket está completa, pero todavía quiero añadir algunos elementos más.

Envío de mensajes al servidor

Dado que se trata de una demostración de traducción, tener más de un idioma es una excelente manera de mostrar el poder de la API de Google Translate junto con los mensajes SMS de Nexmo. He creado un menú desplegable con los idiomas a elegir. Este menú desplegable es donde la comunicación bidireccional ocurre con el servidor, y la aplicación envía el idioma seleccionado desde el cliente.

const languages = [
  { label: "English", value: "en"},
  { label: "French", value: "fr"},
  { label: "German", value: "de"},
  { label: "Spanish", value: "es"}
];

<Select
  labelKey="label"
  onChange={({ option }) => {
    sendMessage(option.value)
    setTranslateValue(option.label)
  }}
  options={languages}
  value={translateValue}
  valueKey="value"
/>

En este caso, la sendMessage función de react-use-websocket es como podemos enviar información de vuelta a nuestro servidor y consumirla. Este proceso es donde el manejador de eventos que configuramos anteriormente es muy útil. Es este desplegable el que determina a qué idioma traduce el mensaje la API de Google Translate y lo muestra en pantalla.

Visualización del estado de la conexión

Como se trata de una demostración en un entorno de conferencia, pensé que sería buena idea disponer de un indicador de conectividad. Mientras el front-end permanezca conectado al WebSocket, la luz se mostrará en verde.

const CONNECTION_STATUS_CONNECTING = 0;
const CONNECTION_STATUS_OPEN = 1;
const CONNECTION_STATUS_CLOSING = 2;

function Status({ status }) {
  switch (status) {
    case CONNECTION_STATUS_OPEN:
      return <>Connected<div className="led green"></div></>;
    case CONNECTION_STATUS_CONNECTING:
      return <>Connecting<div className="led yellow"></div></>;
    case CONNECTION_STATUS_CLOSING:
      return <>Closing<div className="led yellow"></div></>;
    default:
      return <>Disconnected<div className="led grey"></div></>;;
  }
}

//....
<Status status={readyState} />
//...

El componente Status utiliza el botón readyState para cambiar entre los distintos estados e indicárselo al usuario. Si se pone en rojo, significa que algo va mal con el servidor WebSocket y que deberías comprobarlo.

Una vez que todo está en marcha, se parece a esto:

Animation of Working Demo AppAnimation of Working Demo App

Pruébelo

El código de la aplicación de demostración código de la aplicación de demostración está en nuestra organización comunitaria GitHub, y puedes probarlo por ti mismo también. He creado un README que debería ayudarte a realizar la configuración y ejecutarla localmente en tu servidor o desplegarla en Heroku. También he proporcionado un Dockerfile, si usted prefiere ir por ese camino. Hágame saber lo que piensa de ella, y si tiene algún problema, no dude en ponerse en contacto y presentar un problema en el repositorio.

Compartir:

https://a.storyblok.com/f/270183/384x384/444c073b5e/kellyjandrews.png
Kelly J AndrewsAntiguo miembro del equipo

Kelly J Andrews es desarrolladora de Nexmo y lleva más de 30 años jugando con los ordenadores. Utilizó BASIC por primera vez a los 5 años.

No fue hasta que creó su primera página web en 1997 y probó JavaScript por primera vez cuando encontró su verdadera vocación. Kelly lucha ahora por JavaScript, el código comprobable y la entrega rápida.

Se le puede encontrar cantando karaoke, haciendo magia o animando a los Cubs y a los Fighting Irish.