Write translator in Golang
Welcome back! 😀
I have already set up this blog yesterday to write about things that interest me from a technical perspective, like in a diary.
As a Senior Software Engineer at e.GO Mobile, I work also with Go to develop things like tools or microservices.
I do not want to go into too much detail, but demonstrate with a small general example, I wrote yesterday, how simple and powerful this programming language is.
For this purpose, I have set up the following example project on GitHub to show how projects can already be realized with the means available.
This is a command-line tool that uses ChatGPT to translate any kind of text into a target language.
Lets “go” through the code a bit:
I created the project using Go 1.25.x and initialized it with
go mod init github.com/mkloubert/the-gitfather-blog--t-cli
To turn my application into a command-line tool, I used the very popular cobra module by Steve Francia.
This helps to parse arguments for a CLI and make them available in a tree structure, s. main.go:
package main
import (
"fmt"
"os"
"strings"
"github.com/spf13/cobra"
)
// the main function / entry point
func main() {
err := readDotEnvIfAvailable()
if err != nil {
// could not read local ENV file
fmt.Println(err)
os.Exit(2)
}
// we can define the default target language
// with TGF_DEFAULT_LANGUAGE environment variable
TGF_DEFAULT_LANGUAGE := strings.TrimSpace(os.Getenv("TGF_DEFAULT_LANGUAGE"))
if TGF_DEFAULT_LANGUAGE == "" {
TGF_DEFAULT_LANGUAGE = "english" // this is the fallback
}
// storage for: --context / -c
var context string
// storage for: --language / -l
var language string
// the root command
var rootCmd = &cobra.Command{
Use: "t",
Short: "Translates text using ChatGPT",
Long: `A fast and easy-to-use command line tool to translate texts by The GitFather (https://blog.kloubert.dev/)`,
Run: func(cmd *cobra.Command, args []string) {
// ... the execution code of the application
// s. https://github.com/mkloubert/the-gitfather-blog--t-cli/blob/efdf011c47cb799c281264f76e309f68503b4cd1/main.go#L61
// for more information
},
}
// setup flags ...
rootCmd.Flags().StringVarP(&context, "context", "c", "", "additional context information for chat model") // --context / -c flag
rootCmd.Flags().StringVarP(&language, "language", "l", "", "the name of the target language") // --language / -l flag
// run the application and parse
// the command line arguments
// and other kind of data
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
At the entry point of the application, we load environment variables from a .env.local
file in the first step using the GoDotEnv module by John Barton, s. utils.go:
func readDotEnvIfAvailable() error {
_, err := os.Stat(envLocal)
if err == nil {
// load from .env.local
godotenv.Load(envLocal)
} else if !os.IsNotExist(err) {
return err
}
return nil
}
The .env.local
can be (but doesn’t have to be) used to set the environment variable OPENAI_API_KEY
for the OpenAI API. Otherwise, this variable must be set globally, which could be interesting for other projects.
Furthermore, we can use TGF_DEFAULT_LANGUAGE
to set the default target language. Without defining anything, this is set to english
.
Finally, we come to the actual definition of the program:
We set the so-called “root command” and define the following optional parameters:
Argument | Shortcut | Description | Example |
---|---|---|---|
--context |
-c |
provides ChatGPT additional information about the text to be translated … this can be useful if you want to point out the structure of the data to ChatGPT, in order to indicate what should not be translated | --context="do not translate keys in this JSON object" |
--language |
-l |
defines the target language, such as german , urdu , etc., and overwrites TGF_DEFAULT_LANGUAGE |
--language="chinese" |
Now the program can be started via the command line from the project directory:
go run . --context="do not translate keys" --language="french" < ./test.json
That’s how quickly it goes 🚀🚀🚀
Additionally, I would like to go through the code that actually performs the translation, s. openai.go:
func translateWithGPT(textToTranslate string, targetLanguage string, context string) (string, error) {
OPENAI_API_KEY := strings.TrimSpace(os.Getenv("OPENAI_API_KEY"))
if OPENAI_API_KEY == "" {
// we need an API key
return "", fmt.Errorf("missing OPENAI_API_KEY environment variable")
}
promptSuffix := strings.TrimSpace(context)
if promptSuffix != "" {
promptSuffix = fmt.Sprintf(" (%s)", promptSuffix)
}
// setup user message
userMessage := map[string]interface{}{
"role": "user",
"content": fmt.Sprintf(
`Your only job is to translate the following text to %s language by keeping its format without assumptions%s:%s`,
targetLanguage,
promptSuffix,
textToTranslate,
),
}
// collect all messages
var messages []interface{}
messages = append(messages, userMessage)
// setup request body
// s. https://platform.openai.com/docs/api-reference/chat/create
requestBody := map[string]interface{}{
"model": "gpt-3.5-turbo-0125", // we are using GPT 3.5, because it is enough for this usecase
"messages": messages,
"temperature": 0, // with this we tell ChatGPT that is not as less assumptions as possible
}
// create JSON string from object in requestBody
jsonData, err := json.Marshal(requestBody)
if err != nil {
return "", err
}
// start the POST request
request, err := http.NewRequest("POST", chatCompletionV1Url, bytes.NewBuffer(jsonData))
if err != nil {
return "", err
}
// tell API the content type
// and the key (go to https://platform.openai.com/api-keys)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", OPENAI_API_KEY))
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return "", err
}
defer response.Body.Close() // keep sure response.Body is really clsed at the end
if response.StatusCode != 200 {
return "", fmt.Errorf("unexpected status code %v", response.StatusCode)
}
// read all data from response
responseBodyData, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
// parse JSON to ChatResponseV1 object
var chatResponse ChatResponseV1
err = json.Unmarshal(responseBodyData, &chatResponse)
if err != nil {
return "", err
}
// we should have enough data now
return chatResponse.Choices[0].Message.Content, nil
}
In short, I am building a prompt using the Chat Completion API from ChatGPT and inserting information from the command line, such as the text to be translated, there.
// setup user message
userMessage := map[string]interface{}{
"role": "user",
"content": fmt.Sprintf(
`Your only job is to translate the following text to %s language by keeping its format without assumptions%s:%s`,
targetLanguage,
promptSuffix,
textToTranslate,
),
}
promptSuffix
is the optional information that comes from the CLI argument --context
.
As a model, I use gpt-3.5-turbo-0125
, which in my opinion is completely sufficient and cost-effective.
About temperature: 0, I also give ChatGPT the information to be as uncreative as possible with its answer and to solely focus on the task.
Have fun trying it out!