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!