Pavel Oborin
Dec 7, 2018 · 5 min read

Creating a good app is not easy. Whatever unique and useful application you have written, if the user does not like it, then you have, as they say, a big problem. Most people do not like and get scared of everything that they do not understand. Often, the user interface and emails are the visible tip of the iceberg of your application, according to which the user evaluates it. Therefore, the localization of everything that the user sees is extremely important.

Image for post
Image for post
Photo by rawpixel on Unsplash

Remember how ten years ago, when the Internet was just starting to enter the life of the masses, and many of today’s IT-giants were in the stage of startup dwarfs with a couple dozen employees, it was just the way things are to send a letter to the user in English. And users treated it with understanding. Today, when there are all on the Internet and you don’t need to be a rocket scientist, have a university degree or know English to use it, it’s considered mauvais ton not to support localization in your application. By the way, in our company localization of all UI texts is already carried out into 20 languages and the list of supported languages is constantly growing.

In Go, as in a fairly young language, all the contemporary trends in web development are implemented at the level of enclosing packages and do not require additional “bandaid solution”. (I started learning Go a few years ago, but I still remember the feeling of “discovered superpowers” that I experienced the first days after learning the language. It seemed to me that now I can implement any task by writing just a couple of strings.)

Of course, the localization has not been bypassed in Go. Localization in it is available almost “out of the box” with the help of packages: golang.org/x/text/language, golang.org/x/text/message and golang.org/x/text/feature/plural. Let’s look how easy it is in Go in just half an hour, using these packages, you can implement such a non-trivial task as localization of emails.

Looking ahead, I’ll say that the purpose of this article is first of all to show the power and beauty of Go and highlight the basic features of golang.org/x/text/message package for working with localizations. If you’re looking for a solution for a production application, you might want to consider a ready-made library. The advantages of go-i18n are many stars on Github (there is mine among them) and great flexibility. However, there are arguments against its use: you may not need all that flexibility and functionality; why to use an external library when everything is already implemented in the language itself; if you already have your own translation system with its own formats, this library “as it is”, most likely, will not fit and it somehow will have to be modified for yourself; well, in the end, using a third-party library is not as interesting and informative as doing something yourself.

Let’s formulate the main requirements for the task to be implemented. We have: a) tags in yaml format: “label_name: translation text”; the translation language is specified in the file name, for example ru.yml; b) templates of letters in html. It is required based on input parameters: locale and data — to generate the localized text of the letter.

And let’s start… but first a few words about the golang.org/x/text/message package. It is used to format the output of localized strings. Message implements the interface of a standard fmt package and can replace it. Example of use:

message.SetString(language.Russian, “toxic”, “токсичный”)
message.SetString(language.Japanese, “toxic”, “毒性”)
message.NewPrinter(language.Russian).Println(“toxic”)
message.NewPrinter(language.Japanese).Println(“toxic”)
//Results:
//токсичный
//毒性

In order for the package to “see” the tag, you must first declare it. The example uses the SetString function to do this. Next, a printer is created for the selected language and a localized string is displayed directly.

To solve our problem, we could generate a go-file with all the tags, but it is not very convenient, because when you add new tags you will have to regenerate this file and build the application again. Another way to tell message about our tags is to use dictionaries. A dictionary is a structure that implements the tag search interface: Lookup(key string) (data string, ok bool).

Option with dictionaries suits us. First, let’s define the structure of the dictionary and implement the Lookup interface for it:

type dictionary struct {
Data map[string]string
}
func (d *dictionary) Lookup(key string) (data string, ok bool) {
if value, ok := d.Data[key]; ok {
return “\x02” + value, true
}
return “”, false
}

Let’s parse all the tags from yaml files into a collection of dictionaries, which is a map[lang]*dictionary of map format, where lang is a language tag in BCP47 format.

func parseYAMLDict() (map[string]catalog.Dictionary, error) {
dir := “./translations”
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
translations := map[string]catalog.Dictionary{} for _, f := range files {
yamlFile, err := ioutil.ReadFile(dir + “/” + f.Name())
if err != nil {
return nil, err
}
data := map[string]string{}
err = yaml.Unmarshal(yamlFile, &data)
if err != nil {
return nil, err
}
lang := strings.Split(f.Name(), “.”)[0] translations[lang] = &dictionary{Data: data}
}
return translations, nil
}

Install the dictionary collection in the init function so that the dictionaries are used by the message package when the application starts.

func init() {
dict, err := parseYAMLDict()
if err != nil {
panic(err)
}
cat, err := catalog.NewFromMap(dict)
if err != nil {
panic(err)
}
message.DefaultCatalog = cat
}

So, at the moment we have achieved the availability of localization of tags from our yml-files anywhere in the program:

message.NewPrinter(language.Russian).Println(“label_name”)

It’s time to move on to the second part of the task and substitute our localized tags in the email templates. For example, consider a simple message — a welcome email when you register a user: Hello, Bill Smith!

For parsing we use standard package html/template. When parsing templates in html/template, you can set your functions via Funcs():

template.New(tplName).Funcs(fmap).ParseFiles(tplName)

Let’s add a function to the template that will translate tags and substitute variables in them, and name it translate. Template parsing code:

//Localization language
lang:=language.Russian
//Template name
tplName:=”./templates/hello.html”
//Variables in the template
data := &struct {
Name string
LastName string
}{Name: “Bill”, LastName: “Smith”}
fmap := template.FuncMap{
//Tag localization function
“translate”: message.NewPrinter(lang).Sprintf,
}
t, err := template.New(tplName).Funcs(fmap).ParseFiles(tplName)
if err != nil {
panic(err)
}
buf := bytes.NewBuffer([]byte{})
if err := t.Execute(buf, data); err != nil {
panic(err)
}
fmt.Println(buf.String())

The final email template ./templates/hello.html:

<!DOCTYPE html>
<head>
<title>{{translate “hello_subject”}}</title>
</head>
<body>
{{translate “hello_msg” .Name .LastName}}
</body>
</html>

Since we use the Sprintf function in translate for localization, the variables in the tag text will be sewn using the syntax of this function. For example, %s is a string, %d is an integer. Files with tags:

en.yml
hello_subject: Greeting mail
hello_msg: Hello, %s %s!
ru.yml
hello_subject: Welcome letter
hello_msg: Hello, %s %s!

On this, in principle, and all localization of letters is ready! Having written only a few dozen strings of code, we got a powerful functionality that allows us to localize emails of any complexity into dozens of languages.

If you like this example, you can go ahead and implement pluralization yourself, using variable names in tags instead of %s, and using functions in tags. I deliberately did not do it to leave scope for your imagination.

The code shown in the examples is written specifically to demonstrate the capabilities of the message package and does not pretend to be perfect, the full code listing is available on github.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store