🈂️ An easy way to translate your Golang application
Tags: go, tutorial, beginners, showdev
Published: 2021-04-12 22:23:08

## Introduction

よう皆！✌️ Let's talk about one of the important topics, if you're preparing a Go application for a multilingual audience or just need support for different languages in the REST APIs. 

This topic is not as simple as it seems. Because each language has its own special elements in terms of the word form when using numerals. For example, in Russian there are **3** different variants for items quantities: 

- `one`, 1 item;
- `few`, 2 items;
- `many`, 3+ items;

> 🤔 And this must be understood when translating the application!

Don't worry, everything will soon fall into place.

<a name="toc"></a>
### 📝 Table of contents

- [Source code of the project](#source-code-of-the-project)
- [Prepare the project for translation](#prepare-the-project-for-translation)
  - [Website application](#website-application)
  - [Template for display](#template-for-display)
- [Extracting the original language](#extracting-the-original-language)
- [Launch the application and playing with languages](#launch-the-application-and-playing-with-languages)
- [Afterword](#afterword)

<a name="#source-code-of-the-project"></a>
## Source code of the project

Yeah, for those who like to see the code first, I created a repository on GitHub:

{% github koddr/tutorial-go-i18n no-readme %}

[↑ Table of contents](#toc)

<a name="#prepare-the-project-for-translation"></a>
## Prepare the project for translation

I've looked at many packages for this operation (including the one built into the Go core), but [nicksnyder/go-i18n](https://github.com/nicksnyder/go-i18n) was the only one I enjoyed working with in my projects. We will create our demo application using this particular package.

> 👋 Please write in the comments which package for i18n you use and why!

[↑ Table of contents](#toc)

<a name="#website-application"></a>
### Website application

Yes, let's take the [Fiber](https://github.com/gofiber/fiber) web framework as the core for our application, which has excellent template support (with smoothly reload function) and is easy to write and read code.

> 🔥 Please read comments in code!

```go
// ./main.go

package main

import (
	"log"
	"strconv"

	"github.com/BurntSushi/toml"
	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/template/html"
	"github.com/nicksnyder/go-i18n/v2/i18n"
	"golang.org/x/text/language"
)

func main() {
	// Create a new i18n bundle with default language.
	bundle := i18n.NewBundle(language.English)

	// Register a toml unmarshal function for i18n bundle.
	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)

	// Load translations from toml files for non-default languages.
	bundle.MustLoadMessageFile("./lang/active.es.toml")
	bundle.MustLoadMessageFile("./lang/active.ru.toml")

	// Create a new engine by passing the template folder
	// and template extension.
	engine := html.New("./templates", ".html")

	// Reload the templates on each render, good for development.
	// Optional, default is false.
	engine.Reload(true)

	// After you created your engine, you can pass it
	// to Fiber's Views Engine.
	app := fiber.New(fiber.Config{
		Views: engine,
	})

	// Register a new route.
	app.Get("/", func(c *fiber.Ctx) error {
		lang := c.Query("lang")            // parse language from query
		accept := c.Get("Accept-Language") // or, parse from Header

		// Create a new localizer.
		localizer := i18n.NewLocalizer(bundle, lang, accept)

		// Set title message.
		helloPerson := localizer.MustLocalize(&i18n.LocalizeConfig{
			DefaultMessage: &i18n.Message{
				ID:    "HelloPerson",     // set translation ID
				Other: "Hello {{.Name}}", // set default translation
			},
			TemplateData: &fiber.Map{
				"Name": "John",
			},
		})

		// Parse and set unread count of emails.
		unreadEmailCount, _ := strconv.ParseInt(c.Query("unread"), 10, 64)

		// Config for translation of email count.
		unreadEmailConfig := &i18n.LocalizeConfig{
			DefaultMessage: &i18n.Message{
				ID:    "MyUnreadEmails",
				One:   "You have {{.PluralCount}} unread email.",
				Other: "You have {{.PluralCount}} unread emails.",
			},
			PluralCount: unreadEmailCount,
		}

		// Set localizer for unread emails.
		unreadEmails := localizer.MustLocalize(unreadEmailConfig)

		// Return data as JSON.
		if c.Query("format") == "json" {
			return c.JSON(&fiber.Map{
				"name":          helloPerson,
				"unread_emails": unreadEmails,
			})
		}

		// Return rendered template.
		return c.Render("index", fiber.Map{
			"Title":        helloPerson,
			"UnreadEmails": unreadEmails,
		})
	})

	// Start server on port 3000.
	log.Fatal(app.Listen(":3000"))
}
```

[↑ Table of contents](#toc)

<a name="#template-for-display"></a>
### Template for display

Normally, I don't like to take pre-made CSS libraries, but for the simplicity and nice look of this demo, I took the [Bootstrap 5](https://getbootstrap.com/) (`v5.0.0-beta3`) library:

```html
<!-- ./templates/index.html -->

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>{{.Title}}</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-eOJMYsd53ii+scO/bJGFsiCZc+5NDVN2yr8+0RDqr0Ql0h+rP48ckxlpbzKgwra6"
      crossorigin="anonymous"
    />
    <style>
      * {
        font-family: sans-serif;
        color: #333333;
      }
    </style>
  </head>
  <body>
    <div class="col-lg-8 mx-auto p-3 py-md-5">
      <h1>{{.Title}}</h1>
      <br />
      <div class="row g-5">
        <div class="col-md-6">
          <ul class="icon-list">
            <li>{{.UnreadEmails}}</li>
          </ul>
        </div>
      </div>
      <footer class="pt-5 my-5 text-muted border-top">
        Switch to 🇬🇧 <a href="/">English</a>, 🇪🇸
        <a href="/?lang=es">Español</a>, 🇷🇺 <a href="/?lang=ru">Русский</a>.
      </footer>
    </div>
  </body>
</html>
```

[↑ Table of contents](#toc)

<a name="#extracting-the-original-language"></a>
## Extracting the original language

- First, install `goi18n` CLI:

```bash
go get -u github.com/nicksnyder/go-i18n/v2/goi18n
```

- Extract all `i18n.Message` struct literals in our Go source files to a message file for translation (by default, `active.en.toml`):

```bash
goi18n extract
```

```toml
# ./active.en.toml

HelloPerson = "Hello {{.Name}}"

[MyUnreadEmails]
one = "You have {{.PluralCount}} unread email."
other = "You have {{.PluralCount}} unread emails."
```

- Create an empty messages files for the language that you want to add (in this example, `translate.es.toml` and `translate.ru.toml`).

```bash
touch translate.es.toml translate.ru.toml
```

- Run `goi18n merge` command with this messages files to be translated:

```bash
# For Español:
goi18n merge active.en.toml translate.es.toml

# For Russian:
goi18n merge active.en.toml translate.ru.toml
```

- Open messages files and do the lines translation. As you remember from the beginning of this tutorial, the Russian language has its own peculiarities for displaying the number of objects. Therefore, I will give an example of translation for this language:

```toml
# ./translate.ru.toml

[HelloPerson]
hash = "sha1-5b49bfdad81fedaeefb224b0ffc2acc58b09cff5"
other = "Привет, {{.Name}}"

[MyUnreadEmails]
hash = "sha1-6a65d17f53981a3657db1897630e9cb069053ea8"
one = "У вас есть {{.PluralCount}} непрочитанное письмо."
other = "У вас есть {{.PluralCount}} непрочитанных писем."
few = "У вас есть {{.PluralCount}} непрочитанных письма." # <-- new row for "few" count
many = "У вас есть {{.PluralCount}} непрочитанных писем." # <-- new row for "many" count
```

- When all have been translated, rename them to `active.es.toml` and `active.ru.toml` and place to the `./lang` folder.

- That's it!

[↑ Table of contents](#toc)

<a name="#launch-the-application-and-playing-with-languages"></a>
## Launch the application and playing with languages

We're finally ready to launch our application:

```bash
go run main.go

# ┌───────────────────────────────────────────────────┐ 
# │                    Fiber v2.7.1                   │ 
# │               http://127.0.0.1:3000               │ 
# │       (bound on host 0.0.0.0 and port 3000)       │ 
# │                                                   │ 
# │ Handlers ............. 2  Processes ........... 1 │ 
# │ Prefork ....... Disabled  PID ............. 64479 │ 
# └───────────────────────────────────────────────────┘
```

OK. Open `http://localhost:3000/` page:

![go i18n en](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vrb5z6w7sff8snrjczta.png)

As you can see, by default the website will always open in 🇬🇧 **English**, as specified in the application settings. 

> 💡 In Golang unset `int` values will always have `0`, not `null` or `None` as in JavaScript or Python. That's why if we don't specify the `unread` parameter in a query, the template will be set it to `0`.

Next, let's switch language to the 🇪🇸 **Español**. Click to the link at the page bottom and add query parameter `unread` with some integer:

![go i18n es](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/viq1sdvurvifxd0zrl4t.png)

And, go to another language, 🇷🇺 **Russian**:

![go i18n ru](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rgnsj9dzqi8y0xgxause.png)

> 👆 You can play around with the value of `unread` to see how the word form automatically changes after a numeral for these languages.

Also, to demonstrate how JSON works, please add `format=json` parameter to the query to see how [Fiber](https://github.com/gofiber/fiber) web framework will give you the same content, but in JSON format:

![go i18n json](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jtkzefn2jgixliofksea.png)

[↑ Table of contents](#toc)

<a name="#afterword"></a>
## Afterword

In real web applications, you can create different variants of REST API methods to deliver translations to the frontend. But the main thing to remember is that if you do international projects, think about the specifics of the language of those countries in the first place. 

**And Golang will help with everything else!** 🎉

[↑ Table of contents](#toc)

<a name=""></a>
## Photos and videos by

- Vic Shóstak https://shostak.dev

<a name=""></a>
## P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

- 🔥 [gowebly][gowebly_url]: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
- ✨ [create-go-app][cgapp_url]: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
- 🏃 [yatr][yatr_url]: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
- 📚 [gosl][gosl_url]: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
- 🏄‍♂️ [csv2api][csv2api_url]: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
- 🚴 [json2csv][json2csv_url]: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

<!-- Footer links -->

[gowebly_url]: https://github.com/gowebly
[cgapp_url]: https://github.com/create-go-app
[yatr_url]: https://github.com/koddr/yatr
[gosl_url]: https://github.com/koddr/gosl
[csv2api_url]: https://github.com/koddr/csv2api
[json2csv_url]: https://github.com/koddr/json2csv