📜 ⬆️ ⬇️

Localization in Go using basic packages

Creating a good application is not easy. No matter how unique and useful the 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 scares all that they do not understand. Often, the user interface and letters are the visible tip of the iceberg of your application, by which the user assesses it. Therefore, the localization of everything that the user sees is extremely important.


Remember how ten years ago, when the Internet was just beginning to enter the lives of the masses, and many of today's IT giants were in the stage of start-up dwarfs with a couple dozen employees, it was in the nature of things to send the user a letter in English. And users treated this with understanding. Today, when there is everything 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 bad form not to support localization in your application. By the way, in our company, the localization of all UI texts is already being implemented in 20 languages ​​and the list of supported languages ​​is constantly growing.


In Go, as in a fairly young language, all modern trends in web development are implemented at the level of basic packages and do not require additional “tambourine dances”. (I started learning Go a few years ago, but I still remember the feeling of “opened supernormal abilities” that I experienced in the first days after becoming acquainted with this language. It seemed that now I can accomplish any task by writing just a couple of lines.)


Of course, not bypassed in Go side and localization. Localization is available almost out of the box using basic packages: golang.org/x/text/language , golang.org/x/text/message and golang.org/x/text/feature/plural . Let's look at how easy it is in Go in just half an hour, using these packages, you can accomplish such a non-trivial task as the localization of letters.


Looking ahead, I’ll say that the purpose of this article is to first of all show the power and beauty of Go and highlight the basic features of message packages for working with localizations. If you are looking for a solution for a production application, you might be better off with a ready-made library . The advantages of go-i18n are the many stars on github (there are mine among them) and great flexibility. However, there are also arguments against its use: you may not need all that flexibility and functionality; Why 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 is" will most likely not work and you will have to refine it anyway; Well, in the end, using a third-party library is not as interesting and informative as doing something yourself.


We formulate the main requirements for the task to be implemented. There are: a) labels in the yaml format: “label_name: translation text” ; The translation language is specified in the file name, for example, ru.yml; b) letter templates in html. It is necessary based on the input parameters: locale and an array of data - to generate localized text of the letter.


And let's get started ... But first, a few more words about the message package (golang.org/x/text/message). It is designed to format the output of localized strings. Message implements the standard fmt interface and can replace it. Usage example:

message.SetString(language.Russian, "toxic", "") message.SetString(language.Japanese, "toxic", "毒性") message.NewPrinter(language.Russian).Println(“toxic”) message.NewPrinter(language.Japanese).Println(“toxic”) //: // //毒性 

In order for the package to “see” the label, it must first be announced. The example uses the SetString function for this. Next, a printer is created for the selected language and the localized string is output directly.

To solve our problem, we could generate a go-file with all the tags, but this is not very convenient, since when adding new tags we will have to regenerate this file each time 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 Lookup label search interface (key string) (data string, ok bool) .

The variant with dictionaries suits us. First, we 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 } 

Spars all the tags from the yaml files into the dictionary collection, which is a map format map [lang] * dictionary , where lang is a language tag in the 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 collection of dictionaries 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 files anywhere in the program:

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

It is time to move on to the second part of the task and substitute our localized tags in the letter templates. For example, consider a simple message - a welcome letter when registering a user:
Hello, Bill Smith!


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

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

Add a function to the template that will translate labels and substitute variables in them, and call it translate . Template parsing code:

 //  lang:=language.Russian //  tplName:=”./templates/hello.html” //   data := &struct { Name string LastName string }{Name: "Bill", LastName: "Smith"} fmap := template.FuncMap{ //   "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 letter 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 the translate for localization, variables in the label text will be stitched using the syntax of this function. For example, % s is a string, % d is an integer.
Tagged files
en.yml

 hello_subject: Greeting mail hello_msg: Hello, %s %s! 

ru.yml

 hello_subject:   hello_msg: , %s %s! 

On this, in principle, and everything, the localization of letters is ready! Having written only a few dozen lines of code, we received powerful functionality that makes it possible to localize letters of any complexity in dozens of languages.


If you like this example, you can go ahead and independently implement pluralization, use variable names in labels instead of % s variable names, and use functions in labels. I deliberately did not do this to leave room for your imagination.


The code given in the examples was written specifically to demonstrate the capabilities of the message package and does not claim to be ideal, a full code listing is available on github .

Source: https://habr.com/ru/post/431604/


All Articles