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.
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.
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.
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.
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.
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.
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.
- We create a new piece of state to keep track of the translation results
- 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.
- The result of the request is set to a variable named translation
- We update the state of results to be the translated string inside of the translation variable.
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.
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) { //highlight-line
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]) //highlight-line
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.
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) // highlight-line
// 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, // highlight-line
target: language.value,
key: "YOUR_API_KEY",
},
}
)
setResults(translation.data.data.translations[0].translatedText)
}
translate()
}, [language, debouncedText]) // highlight-line
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
Comments
Recent Work
Basalt
basalt.softwareFree desktop AI Chat client, designed for developers and businesses. Unlocks advanced model settings only available in the API. Includes quality of life features like custom syntax highlighting.
BidBear
bidbear.ioBidbear is a report automation tool. It downloads Amazon Seller and Advertising reports, daily, to a private database. It then merges and formats the data into beautiful, on demand, exportable performance reports.