React: Translation Widget

Intro

We will be re-using the dropdown component that we created in the last post, combined with a dynamic query to the Google Translate API to create a translation widget. Here is a preview of the final widget.

translation is working

Scaffolding

Let us scaffold the Translation component. We can start with a basic language selection dropdown which makes use of the Dropdown component that we created in the last post.

components/Translate.js
import React, { useState } from "react"

import Dropdown from "./Dropdown"

const languages = [
  {
    label: "Afrikaans",
    value: "af",
  },
  {
    label: "Arabic",
    value: "ar",
  },
  {
    label: "Hindi",
    value: "hi",
  },
]

const Translate = () => {
  const [language, setLanguage] = useState(languages[0])

  return (
    <div>
      <Dropdown
        label="Select a Language"
        options={languages}
        selected={language}
        onSelectedChange={setLanguage}
      />
    </div>
  )
}

export default Translate

You may have noticed that we are now passing in the label as a prop, where before it was hardcoded.

And then we can add our text input.

components/Translate.js
import React, { useState } from "react"

import Dropdown from "./Dropdown"

const languages = [
  {
    label: "Afrikaans",
    value: "af",
  },
  {
    label: "Arabic",
    value: "ar",
  },
  {
    label: "Hindi",
    value: "hi",
  },
]

const Translate = () => {
  const [language, setLanguage] = useState(languages[0])
  const [text, setText] = useState("")

  return (
    <div>
      <div className="ui form">        <div className="field">          <label> Enter Text</label>          <input value={text} onChange={e => setText(e.target.value)} />        </div>      </div>      <Dropdown
        label="Select a Language"
        options={languages}
        selected={language}
        onSelectedChange={setLanguage}
      />
    </div>
  )
}

export default Translate

Just as another quick reminder, we are using Semantic UI for our CSS on this project so if you are wondering where the CSS classes are coming from, that is the answer.

We can see that we are tracking the state of the text input with with the variable text and every time a change is detected in the input the value of text is changed via the setText function.

Convert Component

Now that we have our Translate component scaffolded out, we are going to create a Convert component that will do the work of converting from one language to another. It is going to take two pieces of information as props, the text that was input, and the language that was selected. It will use that information to construct an API request to the Google Translate API, make an API request, and then send the response of that request back to the Translate component as a prop.

Let us scaffold out the basics of this component. We can use the useEffect hook to watch for changes to the language or text props.

"components/Convert.js"
import React, { useState, useEffect } from "react"

// de-structure language and text props
const Convert = ({ language, text }) => {
  useEffect(() => {
    console.log("language or text has changed")
  }, [language, text])

  return (
    <div>
      <p>{text}</p>
    </div>
  )
}

export default Convert

Currently our convert component is just outputting the exact text that it receives into a paragraph and returning that paragraph. No actual conversion at this point.

Of course we need to actually pass these props into the Convert component, so let us go back to our Translate component.

"components/Translate.js"
import React, { useState } from "react"

import Dropdown from "./Dropdown"
import Convert from "./Convert"

const languages = [
  {
    label: "Afrikaans",
    value: "af",
  },
  {
    label: "Arabic",
    value: "ar",
  },
  {
    label: "Hindi",
    value: "hi",
  },
]

const Translate = () => {
  const [language, setLanguage] = useState(languages[0])
  const [text, setText] = useState("")

  return (
    <div>
      <div className="ui form">
        <div className="field">
          <label> Enter Text</label>
          <input value={text} onChange={e => setText(e.target.value)} />
        </div>
      </div>
      <Dropdown
        label="Select a Language"
        options={languages}
        selected={language}
        onSelectedChange={setLanguage}
      />
      <hr />
      <h3 className="ui header">Output:</h3>
      <Convert text={text} language={language} />    </div>
  )
}

export default Translate

And we can see that as we enter text into the input, the state of text is changed with every keypress and it updates our output below.

input straight to output

Now we just need to re-route this text through a request to the Google Translate API.

Making the API Request

There are a couple things going on here.

  1. We create a new piece of state to keep track of the translation results
  2. We create a helper function (translate) inside of useEffect because useEffect itself cannot be an async function, and we always use async functions for API requests.
  3. The result of the request is set to a variable named translation
  4. We update the state of results to be the translated string inside of the translation variable.
components/Convert.js
import React, { useState, useEffect } from "react"
import axios from "axios"

// de-structure language and text props
const Convert = ({ language, text }) => {
  const [results, setResults] = useState("")

  useEffect(() => {
    const translate = async () => {
      const translation = await axios.post(
        "https://translation.googleapis.com/language/translate/v2",
        {},
        {
          params: {
            q: text,
            target: language.value,
            key: "YOUR_API_KEY",
          },
        }
      )

      setResults(translation.data.data.translations[0].translatedText)
    }

    translate()
  }, [language, text])

  return (
    <div>
      <p>{results}</p>
    </div>
  )
}

export default Convert

And we can see that this is working just great.

translation is working

Throttling the API Request With Debouncing

At this point we are making an API request for every single keystroke, and this is actually a perfect example of when you would want to be throttling your API requests. The Google Translate API is now a paid service so not throttling your requests is going to add up quickly.

We covered the basics of how to throttle a request with a timer in this post:

Ncoughlin: React - Throttling API Requests

However this time we are going to introduce a concept called debouncing. Roughly speaking, debouncing is the proper way to be introducing a timer on our text, and it helps to avoid a react Warning “React Hook useEffect has a missing dependency”.

Briefly, before we get into debounced text, let us talk about why we would be getting this error. Anytime you reference a prop or piece of state inside of a useEffect function, React wants you to reference that function inside of the “dependency array”, which is the array in the second argument of useEffect that determines when it runs.

Keep in mind that this is just a warning, you do not HAVE to follow this. However you can encounter some scenarios where you will get errors that are difficult to de-bug if you do not heed this warning.

So for example if we followed the throttling example from the other post that we linked to above (Ncoughlin: React - Throttling API Requests) we would have something like this in our throttling code:

useEffect(() => {
  const search = async () => {
    const { data } = await axios.get("https://en.wikipedia.org/w/api.php", {
      params: {
        action: "query",
        list: "search",
        origin: "*",
        format: "json",
        srsearch: term,
      },
    })
    setResults(data.query.search)
  }

  // run search immediately if this is initial page load
  if (term && !results.length) {    search()
    // else throttle search requests with timer
  } else {
    // wait 500ms before executing search
    let timeoutID = setTimeout(() => {
      // do not search if input is empty
      if (term) {
        search()
      }
    }, 500)

    // CLEANUP: clear current timer
    return () => {
      clearTimeout(timeoutID)
    }
  }
}, [term])

and if you will note the two highlights, you will see that we have referenced results.length without including it in the dependency array.

Debouncing text is essentially a workaround for this. We can format our code in a way to avoid this dependency warning by creating a new state called debouncedText, which is just the text updated a maximum of once every 500ms, and then referencing the debounced text in our actual API request hook.

components/Convert.js
import React, { useState, useEffect } from "react"
import axios from "axios"

// de-structure language and text props
const Convert = ({ language, text }) => {
  const [results, setResults] = useState([])
  const [debouncedText, setDebouncedText] = useState(text)
  // de-bouncing the search term
  // runs every time the text changes
  useEffect(() => {    const timerId = setTimeout(() => {      setDebouncedText(text)    }, 500)    return () => {      clearTimeout(timerId)    }  }, [text])
  // runs every time language or debouncedText updates
  useEffect(() => {
    const translate = async () => {
      const translation = await axios.post(
        "https://translation.googleapis.com/language/translate/v2",
        {},
        {
          params: {
            q: debouncedText,            target: language.value,
            key: "YOUR_API_KEY",
          },
        }
      )

      setResults(translation.data.data.translations[0].translatedText)
    }

    translate()
  }, [language, debouncedText])
  return (
    <div>
      <p>{results}</p>
    </div>
  )
}

export default Convert

In this instance it was not strictly necessary to use this format because we weren’t referencing anything that did not exist in the dependency array, but it’s good to get used to using this convention.

Github Repo

Ncoughlin: React Widgets