DEV Community

Cover image for πŸˆ‚οΈ An easy way to translate your Golang application
Vic ShΓ³stak
Vic ShΓ³stak

Posted on • Updated on

πŸˆ‚οΈ An easy way to translate your Golang application

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.

πŸ“ Table of contents

Source code of the project

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

GitHub logo koddr / tutorial-go-i18n

πŸ“– Tutorial: An easy way to translate your Golang application

↑ Table of contents

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 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

Website application

Yes, let's take the 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!



// ./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"))
}


Enter fullscreen mode Exit fullscreen mode

↑ Table of contents

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 (v5.0.0-beta3) library:



<!-- ./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>


Enter fullscreen mode Exit fullscreen mode

↑ Table of contents

Extracting the original language

  • First, install goi18n CLI:


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


Enter fullscreen mode Exit fullscreen mode
  • Extract all i18n.Message struct literals in our Go source files to a message file for translation (by default, active.en.toml):


goi18n extract


Enter fullscreen mode Exit fullscreen mode


# ./active.en.toml

HelloPerson = "Hello {{.Name}}"

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


Enter fullscreen mode Exit fullscreen mode
  • Create an empty messages files for the language that you want to add (in this example, translate.es.toml and translate.ru.toml).


touch translate.es.toml translate.ru.toml


Enter fullscreen mode Exit fullscreen mode
  • Run goi18n merge command with this messages files to be translated:


# For EspaΓ±ol:
goi18n merge active.en.toml translate.es.toml

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


Enter fullscreen mode Exit fullscreen mode
  • 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:


# ./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


Enter fullscreen mode Exit fullscreen mode
  • 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

Launch the application and playing with languages

We're finally ready to launch our application:



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 β”‚ 
# β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜


Enter fullscreen mode Exit fullscreen mode

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

go i18n en

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

And, go to another language, πŸ‡·πŸ‡Ί Russian:

go i18n ru

πŸ‘† 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 web framework will give you the same content, but in JSON format:

go i18n json

↑ Table of contents

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

Photos and videos by

P.S.

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

❗️ You can support me on Boosty, both on a permanent and on a one-time basis. All proceeds from this way will go to support my OSS projects and will energize me to create new products and articles for the community.

support me on Boosty

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 main projects that need your help (and stars) πŸ‘‡

  • πŸ”₯ gowebly: A next-generation CLI tool that makes it easy to create amazing web applications with Go on the backend, using htmx, hyperscript or Alpine.js and the most popular CSS frameworks on the frontend.
  • ✨ create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.

Other my small projects: yatr, gosl, json2csv, csv2api.

Top comments (0)