Contents

How to use i18n in golang?

Hi, it’s me again. I may not keep my promise to do a post every month, but in my defense I had to move to another place, which made it harder to pay attention to the blog. Anyway, I’m back, to continue.

Today we are going to talk about i18n. I have to say that it is very interesting for me, because I didn’t know much about it. So . . .

How to use i18n in golang?

First of all, we must know what “i18n” means, which stands for “internationalization”. This allows us to create applications (api, websites, or web services) to be consumed by different clients with different languages. This makes them much more user friendly.

Learn with a example

Let’s suppose that I have an api that works in English and in Spanish. But for both cases the behavior and the functions they offer are the same. Well, so far so good. The problem is that by convention the errors are in English but when my application responds to a client in Spanish, I have to translate it in the code.

Obviusly, there are many diferents way to do that. But the must convenient is use i18n.

  1. Add the “i18n” library to your Golang project. You can do this using the go get command in your terminal:
go get github.com/nicksnyder/go-i18n/v2/i18n
  1. For this case we will have an api in gin that uses a middleware with the Accept-Langue header, which will be checked to know the user’s region.
// SetInstancesTranslate is our middleware
r.Use(pkg.SetInstancesTranslate)
package pkg

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/go-playground/locales/en"
	"github.com/go-playground/locales/es"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	en_translations "github.com/go-playground/validator/v10/translations/en"
	es_translations "github.com/go-playground/validator/v10/translations/es"
	"github.com/nicksnyder/go-i18n/v2/i18n"
	"github.com/pelletier/go-toml/v2"
	"golang.org/x/text/language"
)

type Lang struct {
	Tag  language.Tag
	Lang string
	Path string
}

// Note: you need folder translations into your project
func NewLang(tag language.Tag) Lang {
	path := fmt.Sprintf("translations/active.%s.toml", strings.Split(tag.String(), "-")[0])
	return Lang{
		Tag:  tag,
		Lang: tag.String(),
		Path: path,
	}
}

const (
	EN = "en"
	ES = "es"
)

var (
	UniversalTranslator *ut.UniversalTranslator
)

// NewLanguageTranslator returns a new translator for validation messages based on the selected language.
func (l Lang) NewLanguageTranslator() (ut.Translator, *validator.Validate, error) {
	// Note: the first parameter is the alternative when the translator is not found
    // but it is still necessary to set it afterwards.
	UniversalTranslator = ut.New(en.New(), en.New(), es.New())
	v := validator.New()

	switch l.Lang {
	case EN:
		t, ok := UniversalTranslator.GetTranslator(EN) // en
		if !ok {
			return nil, v, ErrTranslatorNotFound
		}
		en_translations.RegisterDefaultTranslations(v, t)
		return t, v, nil
	case ES:
		t, ok := UniversalTranslator.GetTranslator(ES) // es
		if !ok {
			return nil, v, ErrTranslatorNotFound
		}
		es_translations.RegisterDefaultTranslations(v, t)
		return t, v, nil
	default:
		return nil, v, ErrLanguageNotSupported
	}
}

// NewCustomTranslatorI18n returns a new localizer that uses a custom i18n message file.
// This can be a .toml or .json file. For this example, we'll use toml.
func (l Lang) NewCustomTranslatorI18n() (*i18n.Localizer, error) {
	bundle := i18n.NewBundle(l.Tag)
	bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
	// Here, we could load all the .toml files, but it seems better to only load the required one.
	_, err := bundle.LoadMessageFile(l.Path)
	if err != nil {
		return nil, err
	}

	return i18n.NewLocalizer(bundle, l.Lang), nil
}

func SetInstancesTranslate(c *gin.Context) {
	acceptLanguage := c.GetHeader("Accept-Language")
	lang, err := language.Parse(acceptLanguage)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err.Error())
		return
	}

	newLang := NewLang(lang)
	l, err := newLang.NewCustomTranslatorI18n()
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	t, v, err := newLang.NewLanguageTranslator()
	if err != nil {
		c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	tvl := NewTVLContext(t, v, l)
	c.Set("tvl", tvl)
	c.Next()
}

This structure wraps the validations, the translator for the specified region and the i18n locator to return the correct message.

package pkg

import (
	"github.com/gin-gonic/gin"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	"github.com/nicksnyder/go-i18n/v2/i18n"
)

type TVLContext struct {
	T ut.Translator
	V *validator.Validate
	L *i18n.Localizer
}

func NewTVLContext(
	t ut.Translator,
	v *validator.Validate,
	l *i18n.Localizer,
) TVLContext {
	return TVLContext{T: t, V: v, L: l}
}

func GetTVLContext(c *gin.Context) (TVLContext, error) {
	tvl, ok := c.MustGet("tvl").(TVLContext)
	if !ok {
		return TVLContext{}, ErrInvalidTVL
	}

	return tvl, nil
}

All these files can be improved and even create a package so that this logic is abstracted.

Finally, we would have a handler for our tests.

type UserRequest struct {
	Name     string `json:"name" validate:"required,alpha"`
	Email    string `json:"email" validate:"required,email"`
	Password string `json:"password" validate:"required,lte=8"`
}

func main() {
	r := gin.Default()

	// Accept-Language (en or es) required
	r.Use(pkg.SetInstancesTranslate)
	r.POST("user", func(ctx *gin.Context) {
		tvl, err := gotvl.GetTVLContext(ctx)
		if err != nil {
			ctx.JSON(http.StatusInternalServerError, err.Error())
			return
		}

		var u UserRequest
		if err := ctx.ShouldBindJSON(&u); err != nil {
			ctx.JSON(http.StatusUnprocessableEntity, tvl.L.MustLocalize(&i18n.LocalizeConfig{MessageID: "Request malformed"}))
			return
		}

		if err := tvl.V.Struct(u); err != nil {
			if err, ok := err.(validator.ValidationErrors); ok {
				ctx.JSON(http.StatusBadRequest, err.Translate(tvl.T))
				return
			}

			ctx.JSON(http.StatusUnprocessableEntity, tvl.L.MustLocalize(&i18n.LocalizeConfig{MessageID: "Error unexpected"}))
			return
		}

		ctx.JSON(http.StatusOK, tvl.L.MustLocalize(&i18n.LocalizeConfig{MessageID: "OK", PluralCount: 1})) // 🚀
	})

	if err := r.Run(); err != nil {
		log.Fatal(err)
	}
} 

We have the go logic, but we need to have the translation files somewhere. For this we will use the translations folder.

# Generate translations (en, es)
# Create by definitions

.PHONY: init
init:
	mkdir translations && cd translations; touch active.en.toml active.es.toml

.PHONY: gen
gen:
	cd translations && goi18n merge active.en.toml active.es.toml 

# Use the Finish command only when all translations have been completed.
.PHONY: finish
finish:
	cd translations; echo "\n" >> active.es.toml; cat translate.es.toml >> active.es.toml;

.PHONY: reset
reset: 
	cd translations; rm -rf active.es.toml translate.es.toml; touch active.en.toml

How to run?

  1. Run make init. This command to create folder translations and two files: active.en.toml and active.es.toml

  2. Add the necessary messages in active.en.toml, for example:

["Error unexpected"]
one = "Error unexpected"
other = "Error unexpected"

[OK]
one = "OK 200"
other = "OK 200"

["Request malformed"]
one = "Failed to binding request"
other = "Failed to binding request"
  1. Run make gen. This command make active.es.toml to translate.es.toml and in this file we will to add spanish translations, for example:
[OK]
hash = "sha1-3c44e60f668525352909fc17dc5d4ce4c3fbe4f6"
one = "Todo bien 🚀"
other = "Todo bien 👍"
  1. Run make finish. This command move the content from translate.es.toml to active.es.toml

  2. Ready! We have the translation files, now let go do the magic.

Demo

You can find a package created by me to use it in your project in a very simple way. I hope it helps, it is a complement to the post.

Repository: github.com/juanmachuca95/gotvl

Demo: video_demo

Conclusion

In summary, i18n in Go allows for improved user experience, better code organization, increased flexibility and reach, and easier compliance with local language and cultural requirements.

Thank you very much for making it this far. 😍