https://a.storyblok.com/f/270183/120522/e0255364e5/e_sms-translations_1200x600.png

Demonstração de SMS em tempo real com React, Node e Google Translate

Publicado em May 24, 2021

Tempo de leitura: 8 minutos

No ano passado, trabalhei com a API do Google Translate para traduzir mensagens de SMS. Depois de mostrar o resultado para o resto da equipe, eles queriam uma demonstração que pudessem apresentar a outros desenvolvedores nas conferências das quais participávamos. Com base nisso, decidi criar um front-end com React que pudesse exibir as traduções em tempo real.

Criação do WebSocket

O que é um WebSocket?

Para esta demonstração, decidi que usar um WebSocket seria uma ótima solução. Se você ainda não usou um WebSocket antes, trata-se de um protocolo que permite que um cliente e um servidor se comuniquem em tempo real. Os WebSockets são bidirecionais, o que significa que tanto o cliente quanto o servidor podem enviar e receber mensagens. Quando você se conecta a um WebSocket pela primeira vez, a conexão é estabelecida por meio da atualização do protocolo HTTP para o protocolo WebSocket e é mantida ativa enquanto não houver interrupção. Uma vez estabelecida, ela fornece um fluxo contínuo de conteúdo. Exatamente o que precisamos para receber mensagens SMS traduzidas.

Criar o servidor WebSocket no Node

Como primeiro passo para criar os WebSockets, o servidor precisa de um caminho para permitir as conexões dos clientes. Partindo do arquivo original do servidor da minha post anterior, podemos fazer algumas pequenas alterações para criar o servidor WebSocket e os eventos e ouvintes exigidos pelo cliente.

Usando o ws pacote no NPM, podemos criar rapidamente o que precisamos para que isso funcione.

npm install ws

Após a instalação, inclua o pacote no arquivo do seu servidor e crie o servidor WebSocket. WS Permite uma path opção para definir a rota que o cliente usa para se conectar.

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

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

Com esse trecho de código, o cliente agora tem um ponto de conexão com a rota do WebSocket /socket. Com o servidor pronto para funcionar, agora você precisa ficar à espera de um connection evento. Quando o cliente se conecta, o servidor usa o seguinte para configurar os outros ouvintes de que precisamos:

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

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

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

});

Há dois pontos principais a serem destacados:

  1. Ao estabelecer a conexão, definimos a propriedade isAlive para truee ficamos à espera do pong evento. Esse evento serve para que o servidor verifique e mantenha a conexão com o cliente. O servidor envia um ping e responde com pong para verificar se a conexão ainda está ativa.

  2. Aqui, defino translateTo como uma propriedade para armazenar. translateTo É definido por cada cliente por meio de um menu suspenso. Quando alguém que estiver usando nosso aplicativo de demonstração no estande seleciona um idioma diferente, essa ação define essa propriedade para traduzir as mensagens de SMS para o idioma solicitado.

Mantendo a conexão viva

Um aspecto essencial a ser levado em conta é verificar se há clientes que se desconectam. É possível que, durante o processo de desconexão, o servidor não perceba isso, o que pode causar problemas. Com um bom amigo setInterval(), podemos verificar se nossos clientes ainda estão conectados e reconectá-los, se necessário.

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

Envio de mensagens ao cliente

Agora que o WebSocket está conectado e sendo monitorado, podemos lidar com as mensagens recebidas da Nexmo, a tradução e a resposta ao cliente. O método handleRoute precisa ser atualizado em relação à sua versão original para incluir a resposta 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();
  }

};

O wss.clients.forEach método percorre cada conexão e envia os parâmetros de SMS da Nexmo para a API do Google Tradutor. Assim que a tradução for recebida, podemos decidir quais dados o front-end deve receber e devolvê-los como uma string, como fiz aqui com client.send(JSON.stringify(response)).

Para recapitular o que aconteceu até aqui: cada cliente se conecta ao servidor WebSocket chamando a /socket rota e estabelecendo uma conexão. Uma mensagem SMS é enviada do celular do remetente para a Nexmo, que então chama a /inboundSMS rota. O aplicativo encaminha a mensagem de texto para a API do Google Tradutor para cada cliente conectado e, por fim, a envia de volta para a interface do usuário do cliente.

Diagram of WebSocket FlowDiagram of WebSocket Flow

A seguir, vamos criar os componentes da interface do usuário para exibi-lo na tela.

WebSockets com React

Com o servidor WebSocket em execução, podemos passar para a exibição das mensagens na tela. Como gosto de usar Reacte, mais importante ainda, React Hooks, resolvi procurar algo que me ajudasse a me conectar a WebSockets. E, como era de se esperar, encontrei um que atendia exatamente às minhas necessidades.

A interface do aplicativo de demonstração foi desenvolvida com create-react-app, e eu usei o framework Grommet . Esses assuntos estão fora do escopo deste post, mas você pode baixar meu código-fonte e acompanhar o passo a passo.

Conectando-se ao WebSocket

O primeiro passo aqui é estabelecer uma conexão e iniciar uma comunicação bidirecional. O módulo que encontrei é react-use-websocket, e ele tornou essa configuração super simples.

npm install react-use-websocket

Existem inúmeras bibliotecas de hooks do React disponíveis que ajudam a criar funcionalidades impressionantes em pouco tempo. Nesse caso, bastou importar o módulo e definir alguns itens de configuração para estabelecer a conexão.

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);

  //...
}

No componente, importamos o useWebSocket método para passar a URL do WebSocket e o objeto STATIC_OPTIONS como segundo argumento. O useWebSocket método é um hook personalizado que retorna o sendMessage método, lastMessage objeto do servidor (que são nossas mensagens traduzidas) e o readyState que é um número inteiro que nos fornece o status da conexão.

Recebimento de mensagens

Assim que react-use-websocket estabelecida a conexão com o servidor, podemos começar a ouvir as mensagens da lastMessage propriedade. Ao receber mensagens do servidor, elas são exibidas aqui e atualizam o componente. Se o seu servidor tiver vários tipos de mensagem, você poderá identificar essa informação aqui. Como temos apenas um, a implementação é mais simples.

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>
)

O gancho embutido useEffect é executado sempre que o estado é atualizado. Quando lastMessage não for nulo, ele adiciona a nova mensagem ao final da matriz de estado da mensagem anterior, e a interface do usuário é atualizada por meio da map função para renderizar todas as mensagens. Ele está no messageHistory onde todas as strings JSON que recebemos do servidor são armazenadas. A funcionalidade principal do nosso WebSocket está concluída, mas ainda quero adicionar mais alguns itens.

Envio de mensagens ao servidor

Como se trata de uma demonstração de tradução, oferecer mais de um idioma é uma excelente maneira de mostrar o potencial da API do Google Tradutor em conjunto com as mensagens SMS da Nexmo. Criei um menu suspenso com as opções de idiomas disponíveis. É por meio desse menu que ocorre a comunicação bidirecional com o servidor, e o aplicativo envia o idioma selecionado pelo usuário.

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

Aqui, a sendMessage função de react-use-websocket mostra como podemos enviar informações de volta ao nosso servidor e utilizá-las. É nesse processo que o manipulador de eventos que configuramos anteriormente se torna útil. É esse menu suspenso que determina para qual idioma a API do Google Tradutor traduz a mensagem e a exibe na tela.

Exibição do status da conexão

Como se trata de uma demonstração em um ambiente de conferência, achei que seria uma boa ideia ter um indicador de conectividade. Enquanto o front-end permanecer conectado ao WebSocket, a luz ficará 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} />
//...

O Status componente usa o readyState para alternar entre os diversos status e indica isso ao usuário. Se ele ficar vermelho, você sabe que há algo errado com o servidor WebSocket e deve verificar o que está acontecendo.

Quando tudo estiver funcionando, ficará mais ou menos assim:

Animation of Working Demo AppAnimation of Working Demo App

Experimente

O código do aplicativo de demonstração está na nossa organização da comunidade no GitHub, e você também pode experimentá-lo por conta própria. Criei um arquivo README que deve ajudar você a passar pela configuração e executá-lo localmente no seu servidor ou implantá-lo no Heroku. Também disponibilizei um Dockerfile, caso prefira seguir esse caminho. Me diga o que você achou e, se tiver alguma dificuldade, fique à vontade para entrar em contato e abrir um ticket no repositório.

Compartilhar:

https://a.storyblok.com/f/270183/384x384/444c073b5e/kellyjandrews.png
Kelly J AndrewsEx-membro da equipe

Kelly J Andrews é defensora de desenvolvedores da Nexmo e vem se dedicando à informática há mais de 30 anos, tendo usado BASIC pela primeira vez aos 5 anos de idade.

Foi só ao criar sua primeira página da web, em 1997, e ao experimentar o JavaScript pela primeira vez que ele descobriu sua verdadeira vocação. Hoje, Kelly luta pelo JavaScript, pelo código testável e pela entrega rápida.

É possível encontrá-lo cantando no karaokê, fazendo truques de mágica ou torcendo pelos Cubs e pelos Fighting Irish.