Локализация в Go с помощью базовых пакетов

  • Tutorial

Создать хорошее приложение непросто. Какое бы уникальное и полезное приложение вы ни написали, если оно не нравится пользователю, то у вас, как говорится, a big problem. Большинству людей не нравится и отпугивает все, что им непонятно. Зачастую пользовательский интерфейс и письма — это та видимая верхушка айсберга вашего приложения, по которой пользователь его оценивает. Поэтому локализация всего, что видит пользователь, крайне важна.


Вспомните, как еще десять лет назад, когда интернет только начинал входить в жизнь масс, а многие сегодняшние IT-гиганты находились в стадии стартап-карликов с парой десятков сотрудников, в порядке вещей было отправить пользователю письмо на английском. И пользователи относились к этому с пониманием. Сегодня же, когда в интернете присутствуют все и не нужно быть семи пядей во лбу, иметь высшее образование или знать английский, чтобы им пользоваться, считается дурным тоном не поддерживать в своем приложении локализацию. К слову, в нашей компании локализация всех текстов UI уже осуществляется на 20 языков и список поддерживаемых языков постоянно растет.


В Go, как в довольно молодом языке, все современные тренды веб-разработки реализованы на уровне базовых пакетов и не требуют дополнительных «танцев с бубном». (Я начал изучать Go несколько лет назад, но до сих пор помню то ощущение «открывшихся сверхспособностей», которое испытывал первые дни после знакомства с этим языком. Казалось, теперь я могу реализовать любую задачу, написав всего пару строк.)


Конечно, не обошли в Go стороной и локализацию. Локализация в нем доступна практически «из коробки» с помощью базовых пакетов: golang.org/x/text/language, golang.org/x/text/message и golang.org/x/text/feature/plural. Давайте рассмотрим, как просто в Go всего за полчаса, используя эти пакеты, можно реализовать такую нетривиальную задачу, как локализация писем.


Забегая вперед скажу, что цель этой статьи — в первую очередь показать мощь и красоту Go и осветить базовые возможности пакеты message по работе с локализациями. Если вы ищете решение для приложения на продакшене, возможно, вам лучше подойдет готовая библиотека. Преимущества go-i18n — множество звезд на github (среди них есть и моя) и большая гибкость. Впрочем, найдутся и аргументы против ее использования: вам может быть и не нужна вся та гибкость и функциональность; зачем использовать внешнюю библиотеку, когда все уже реализовано в самом языке; если у вас уже существует своя система переводов со своими форматами, эта библиотека «как есть», скорее всего, не подойдет и ее так или иначе придется дорабатывать под себя; ну и, в конце концов, использовать стороннюю библиотеку не так интересно и познавательно, как сделать что-то самому.


Сформулируем основные требования к реализуемой задаче. Имеются: а) метки в формате yaml: “label_name: translation text”; язык перевода задается в названии файла, например ru.yml; б) шаблоны писем в html. Необходимо на основе входных параметров: locale и массива с данными — генерировать локализованный текст письма.


И приступим… Но сперва еще несколько слов о пакете message (golang.org/x/text/message). Она предназначена для форматирования вывода локализованных строк. Message реализует интерфейс стандартного пакета fmt и может ее заменять. Пример использования:

message.SetString(language.Russian, "toxic", "токсичный")
message.SetString(language.Japanese, "toxic", "毒性")
message.NewPrinter(language.Russian).Println(“toxic”)
message.NewPrinter(language.Japanese).Println(“toxic”)
//Результат:
//токсичный
//毒性

Чтобы пакет «видел» метку, ее необходимо предварительно объявить. В примере для этого используется функция SetString. Далее создается printer для выбранного языка и выводится непосредственно локализованная строка.

Для решения нашей задачи мы могли бы генерировать go-файл со всеми метками, но это не очень удобно, так как при добавлении новых меток придется каждый раз перегенерировать этой файл и билдить приложение заново. Другой способ сообщить message о наших метках — использовать словари. Cловарь — это структура, реализующая интерфейс поиска метки Lookup(key string) (data string, ok bool).

Вариант со словарями нам подходит. Для начала определим структуру словаря и реализуем для него интерфейс Lookup:

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
}

Спарсим все метки из yaml-файлов в коллекцию словарей, представляющую собой мапу формата map[lang]*dictionary, где lang — тег языка в формате BCP47.

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
}

Установим коллекцию словарей в init-функции, чтобы словари стали использоваться пакетом message при старте приложения.

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

Итак, на текущий момент мы добились доступности локализации меток из наших файлов в любом месте программы:

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

Настало время перейти ко второй части задачи и подставить наши локализованные метки в шаблоны писем. Для примера рассмотрим простое сообщение — приветственное письмо при регистрации пользователя:
Hello, Bill Smith!


Для парсинга используем другой стандартный пакет — html/template. При парсинге шаблонов в template можно задать свои функции через .Funcs():

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

Добавим в шаблон функцию, которая будет переводить метки и подставлять в них переменные, и назовем ее translate. Код парсинга шаблона:

//Язык локализации
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())

Итоговый шаблон письма ./templates/hello.html:

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

Так как в translate для локализации мы используем функцию Sprintf, то переменные в текст меток будут зашиваться с использованием синтаксиса этой функции. Например, %s — строка, %d — целое число.
Файлы с метками
en.yml

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

ru.yml

hello_subject: Приветственное письмо
hello_msg: Привет, %s %s!

На этом, в принципе, и все, локализация писем готова! Написав лишь несколько десятков строк кода, мы получили мощную функциональность, дающую возможность локализовывать письма любой сложности на десятки языков.


Если вам понравился данный пример, вы можете пойти дальше и самостоятельно реализовать плюрализацию, использование для переменных в метках вместо %s названий переменных и использование функций в метках. Я сознательно не стал этого делать, чтобы оставить простор для вашей фантазии.


Приведенный в примерах код написан специально для демонстрации возможностей пакета message и не претендует на идеальность, полный листинг кода доступен на github.

Share post

Comments 2

    +2

    Как осуществляется взаимодействие с переводчиками при таком подходе?

      0

      Честно сказать — для статьи на хабр это очень слабо.


      Тема не раскрыта, было бы интересно посмотреть на решение всех типичных проблем локализации с использованием стандартных библиотек — изменения структуры фраз/порядка слов, множественное/единственное число, числительные, взаимодействие с переводчиками и поддержка привычных им форматов файлов…


      Совет использовать функцию init(), да ещё и файлы в ней читать — просто вредный. Современный Go старается обходиться без глобальных переменных и init(): https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html


      Ну и поплывшее форматирование кода в статье тоже не добавляет ей качества.

      Only users with full accounts can post comments. Log in, please.