Build a language translator app powered by OpenAI
Gadget
Posted on September 22, 2023
TL;DR: Build an AI-powered translator app using Gadget’s built-in OpenAI connection to quickly localize any web copy.
With the public release of OpenAI’s ChatGPT, developers are suddenly able to apply simple solutions to traditionally complex problems. Accurate, quick, and affordable language translation has been a problem for a long time. Whether you’re an ecommerce merchant looking to maximize the number of potential buyers, or you just have to deal with customer support for a global client base, tons of businesses run into the issue. Thankfully, OpenAI has made it easy to build your own language translation chatbot using a single API call. With Gadget, you can develop and deploy a prototype to production in a matter of minutes.
Follow along with these steps to build your very own language translator.
Step 1: Create a new AI app
Start by going to gadget.new to create a new Gadget app.
Gadget is a hosted development platform that allows you to build apps on the same infrastructure you use to host your production services. Every Gadget app comes with a built-in OpenAI connection and $50 of OpenAI credits, so you can immediately start building your AI apps.
For this guide, select the AI app project, enter your domain, and click Get started to create a new Gadget app. Once the development environment is set up, you can test the OpenAI connection right away. If you open your development app by clicking on the URL in the top right corner (hint: it has –development in the domain) you can log in using Gadget-supplied Google OAuth credentials to try out the simple chat widget.
This widget calls a /chat HTTP route which uses the OpenAI connection to make a request to OpenAI’s completion API. A response will be streamed back to you through the route.
You can modify this existing frontend page and HTTP route to act as a simple language translator instead by adding a translation data model.
Step 2: Add a translation data model
Start by adding a custom data model to your app. This isn’t strictly necessary for this build, but it does give a nice history of translations that could be used as a chat history. Models in Gadget are similar to tables in a database. You can add fields, which are similar columns, to define what kind of data is stored by your model.
Name your model translation
Add an input field to store text entered by users that will be translated
• Make sure it is a string type
• Add a Required validationAdd an output field to store the translated text
• Make sure it is a string type
• Add a Required validationAdd a language field to store the output language that OpenAI translated the original text into
• Make sure this is also a string type
• Add a Required validation
• If desired, add a default language, for example, FrenchAdd a user field to relate translations to the signed-in user who made the request
• Make this a belongs to relationship, so that user has many translations
• Name the field added to the user model to capture the inverse of the relationship translations
Now that you have your custom data model, you can modify the existing /chat route to send a translation prompt to OpenAI and stream the response back to the caller. Data models in Gadget also come with an auto-generated API. You will use the translation model’s create action to save translations in the modified HTTP route below.
Step 3: Customize the /chat HTTP route
Your AI app project comes with an OpenAI connection that has already been set up for you using a Gadget-supplied OpenAI API key. You can see where this connection is being called in the routes/POST-chat.js file.
This is a sample HTTP route that shows how you can use the OpenAI connection and stream a response back to your client. With some modifications to the prompt and input params, you can make this /chat route a hub for your custom translations.
Paste the following code into routes/POST-chat.js, replacing the existing template:
import { RouteContext } from "gadget-server";
import { openAIResponseStream } from "gadget-server/ai";
/**
* Route handler for POST chat
*
* @param { RouteContext } route context - see: https://docs.gadget.dev/guides/http-routes/route-configuration#route-context
*
*/
export default async function route({ request, reply, api, logger, connections, session }) {
// get the text input and target language from the request
const { input, language } = request.body;
// use the OpenAI connection to make a request against OpenAI's completions API
const stream = await connections.openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{
role: "system",
content: `You are a helpful language translator. Please translate the following into ${language}.`,
},
{
role: "user",
content: input,
},
],
stream: true,
});
// a callback function that is called once the stream reponse is complete
// creates a new translation record in the database
const onComplete = (output) => {
void api.translation.create({
input,
language,
output,
user: {
_link: session.get("user")
}
})
}
// send the response back, using Gadget's openAIResponseStream helper to manage the stream
await reply.send(openAIResponseStream(stream, { onComplete }));
}
This code will send a prompt for translation to OpenAI using the OpenAI connection and the response will be streamed back using Gadget’s openAIResponseStream helper. An onComplete callback function will be called once the stream is finished, and a new translation record will be created in the database. This is making use of the translation model’s auto-generated create action.
The new translation record is linked to the current user by accessing the user’s id through the current session. This is a preferred way of linking records to users, as sending the user ID via a parameter is easy to spoof.
The OpenAI connection uses Gadget’s OpenAI API keys, and new teams get $50 of OpenAI credit to use when starting development. See the OpenAI API reference for more details on the completions API being used.
This should be all that is needed for an HTTP route, the final step is building a React frontend.
Step 4: Build your frontend and call /chat
The final step is building a frontend that allows for language translation. Gadget apps come with a React frontend powered by Vite, as well as a built-in authentication system that can be used out of the box. You can modify the default signed-in
frontend route to better support language translations.
Note: The signed-in route is defined in frontend/App.jsx, and is wrapped with one of Gadget’s auth helpers so that only users who have authenticated can access the route.
Paste the following code into frontend/routes/signed-in.jsx:
import { useCallback, useState } from "react";
import { useFetch } from "@gadgetinc/react";
export default function () {
const [input, setInput] = useState("");
const [language, setLanguage] = useState("");
const [{ data: translation, fetching: fetchingTranslation, error }, translate] = useFetch("/chat", {
method: "post",
body: JSON.stringify({ language, input }),
headers: {
"content-type": "application/json",
},
stream: "string"
});
const handleTranslate = useCallback(
async (e) => {
e.preventDefault();
void translate({ input, language });
},
[input, language]
);
return (
<form onSubmit={handleTranslate}>
<div id="appContainer">
<div className="translationSection">
English
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Enter text"
className="textareaRules"
/>
</div>
<div className="translationSection">
<input value={language} onChange={(e) => setLanguage(e.target.value)} />
<textarea
disabled
className="textareaRules translationText"
placeholder={fetchingTranslation ? "Translating..." : ""}
value={translation || ""}
/>
</div>
</div>
<button className="translateButton" type="submit">
Translate
</button>
</form>
);
}
And then paste the following into frontend/App.css:
.app {
width: 100vw;
height: 100vh;
background-image: url("./assets/default-background.svg");
left: 0;
top: 0;
position: fixed;
z-index: 0;
}
.app-content {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
background: radial-gradient(
ellipse at center,
white 0.5rem,
rgb(0, 0, 0, 0) 40rem
);
}
.main {
min-width: 400px;
display: flex;
font-family: system-ui;
align-items: center;
flex-direction: column;
gap: 16px;
}
.header {
display: flex;
background-color: #fff;
width: 100%;
height: 80px;
padding: 21px 50px 21px 49px;
justify-content: space-between;
align-items: center;
font-family: system-ui;
z-index: 1;
position: relative;
font-size: 14px;
}
.header-content {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
color: #000;
font-size: 24px;
font-weight: 600;
line-height: 30px;
}
.signin-button {
display: flex;
padding: 12px 16px;
align-items: center;
justify-content: center;
gap: 12px;
align-self: stretch;
background-color: #fff;
border-radius: 4px;
border: 1px solid #e2e2e2;
cursor: pointer;
text-decoration: none;
}
.signout-button {
display: flex;
padding: 8px 24px;
align-items: flex-start;
gap: 10px;
border-radius: 4px;
background: #000;
color: #fff;
font-size: 16px;
border: none;
cursor: pointer;
}
#appContainer {
display: flex;
gap: 1rem;
min-height: 200px;
}
.translationSection {
display: flex;
flex-direction: column;
min-width: 350px;
}
.textareaRules {
resize: none;
overflow-y: scroll;
border: 1px solid gray;
border-radius: 5px;
padding: 10px;
height: 100%;
margin-top: 1rem;
}
.translationText {
text-decoration: none;
}
.translationText:hover {
cursor: default !important;
}
.translateButton {
padding: 8px 24px;
margin-top: 8px;
border-radius: 4px;
background: #000;
color: #fff;
font-size: 16px;
border: none;
cursor: pointer;
}
form {
display: flex;
flex-direction: column;
}
.logo-big {
color: #000;
font-size: 72px;
font-weight: 600;
line-height: 30px;
}
.app-logo {
height: 5rem;
margin: 0 auto;
display: block;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.app-logo {
animation: app-logo-spin infinite 20s linear;
}
}
@keyframes app-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
If you go back to your development environment you should see a page that allows you to enter some text, enter a language, and then a Translate button that will fire off a call to the /chat route.
Gadget has React helpers that help you call your backend actions and HTTP routes. In this app, the useFetch hook is used to call the /chat route. You can make a request to an HTTP route in a Reactful way, and handle the returned response using the data param. There are also returned values for fetching, a boolean that will be true when the request is underway, and an error, which will return any error messages. The other returned value from the hook is a function, called translate here, which allows you to actually make the call to the route. The translate function is then called in a callback function fired on form submission.
The body of the request is a JSON string of the inputted language and input text, and a stream: “string” property is also set so the hook can handle the stream returned from the route.
// the useFetch is used to define the request to the /chat route, and manage any returned data or errors
const [{ data: translation, fetching: fetchingTranslation, error }, translate] = useFetch("/chat", {
method: "post",
body: JSON.stringify({ language, input }),
headers: {
"content-type": "application/json",
},
stream: "string"
});
// callback function fired on form submission that calls the translate function with the entered input and language
const handleTranslate = useCallback(
async (e) => {
e.preventDefault();
void translate({ input, language });
},
[input, language]
);
Now you can test out your translator app. Enter some text and a target language, and click Translate. The /chat route should stream a translated response back to the frontend. You can also verify that your translation record was saved. Click on the Translation data model in Gadget and then the Data page, you should see records for your translations.
This is a simple frontend design, but you could integrate the useFetch hook and HTTP route into a variety of other apps, such as a customer support chatbot.
Step 5: Deploying to production
You are finished building your app, congrats! You can deploy to production right away for testing by clicking on the Deploy button in the bottom right of the Gadget editor, and confirming that you want to deploy. Your app will be optimized for production, the bundle will be minified and built, and in no time at all you can test out your production environment at your app domain!
There are a couple of things you need to do before you release your production app to users:
- Add your own OpenAI API key to the OpenAI connection
- Go to Connections -> click the edit icon next to GADGET_KEYS in the OpenAI connection
- Enter your API key
- Set up your own Google OAuth credentials
Neither of these are absolutely necessary, but note that using Gadget managed API keys or OAuth credentials will mean that anyone can sign in to your app and use your OpenAI credits! You can still push to production if you are just testing, but make sure you don’t give many users access to your app until you take these steps!
Questions
Have questions about building with Gadget, or Gadget’s OpenAI connection? Feel free to reach out on our developer Discord or read through our docs.
Posted on September 22, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 5, 2023