Локализация golang приложения

Для большинства web приложений требуется локализация, но не стоит забывать о правилах языка. Например не правильно будет просто использовать ключ и подставлять выражение в зависимости от языка, тогда получится что-то вида у вас осталось 5 минут(а/ы). Для того чтобы описать такие правила можно использовать свой формат как это было раньше в symfony/translate или использовать icu формат. Для других языков есть готовые решение например icu4c.

В Golang есть несколько вариантов:

  • написать свой, хорошее решение но долно, необходимо учитывать множество вариантов.
  • использовать библиотеку от uber. Вы получаете зависимость от cgo, что не всегда удобно. Также там нет переводом в только общие правила для формирования даты, валюты и тп.
  • есть и другие библиотеки, например go-i18n. Но там можно уже писать переводы, с возможными вариантами, поддерживается toml.
  • Также есть библиотека от команды го text тоже поддерживает не все, например нет формирования даты в зависимости от локали, но как минимум нет зависимости от cgo.
package main

import (
    "golang.org/x/text/feature/plural"
    "golang.org/x/text/language"
    "golang.org/x/text/message"
    "golang.org/x/text/message/catalog"
)


func trans(){
    lang, _ := language.Parse("ru")
    _ = message.Set(language.Russian, "Hello {name}", catalog.String("Привет %s"))
    hello := message.NewPrinter(lang).Sprintf("Hello {name}","Andrey")
    //Output: Привет Andrey
}

В первой строке мы получаем данные о языке и регионе, например из url. Вторая строка использует глобальный каталог и описывает перевод по ключу, я специально для ключа использовал формат {name} обычно так описывают правила для icu, но для формата необходимо использовать формат fmt. В итоге мы получаем перевод используя ключ для перевода и переменную. Учитывайте что ключи должны быть идентичны.

package main

import (
    "golang.org/x/text/feature/plural"
    "golang.org/x/text/language"
    "golang.org/x/text/message"
)


func main(){

    lang, _ := language.Parse("ru")
    _ = message.Set(language.Russian, "You are {minute} minute(s) late.",
        plural.Selectf(1, "",
            plural.One, "Вы опоздали на одну минуту.",
            plural.Few, "Вы опоздали на %v минуты.",
            plural.Other, "Вы опоздали на %v минут.",
            "=1", "Вы опоздали на одну минуту.",
        ),
    )
    _ = message.Set(language.English, "You are {minute} minute(s) late.",
        plural.Selectf(1, "",
            plural.One, "You are 1 minute late.",
            plural.Other, "You are %v minutes late."
        ),
    )

    hello := message.NewPrinter(lang).Sprintf("You are {minute} minute(s) late.",1)
     // Output: Вы опоздали на одну минуту.
}

В данном варианте мы уже используем выбор перевода в зависимсоти от значения. В английском варианте используется меньше вариантов чем в русском. Можно использовать специальные значения например plural.One для вариантов заканчивающихся на 1 например 101,1001 и тп, или указывать точные значения =1,. Подробнее можно прочитать в Plural Rules.
Также данная библиотека поддерживает валюту и числа с разными форматами. Один из минусов я считаю что библиотека плохо расширяема, очень много интерфейсов и типов internal и не самый удобный интерфейс. Также можно добавить свой словарь переводов для этого достаточно реализовать метод Lookup(key string) (data string, ok bool) и заменить каталог по умолчанию.

package translate

import (
	"context"
	"fmt"
	"log"

	"gitoa.ru/go-4devs/translation"
	"gitoa.ru/go-4devs/translation/arg"
	"gitoa.ru/go-4devs/translation/icu"
	"golang.org/x/text/language"
	"golang.org/x/text/message"
	"golang.org/x/text/message/catalog"
)

func main() {
	err := message.Set(language.Russian, "Hello {city}", catalog.String("Привет %s"))
	if err != nil {
		log.Fatal(err)
	}

	err = message.Set(language.Russian, "It costs {cost}", catalog.String("Это стоит %.2f."))
	if err != nil {
		log.Fatal(err)
	}

	lang, err := language.Parse("ru")
	if err != nil {
		log.Fatal(err)
	}

	ctx := translation.WithLanguage(context.Background(), lang)

	tr := icu.Trans(ctx, "Hello {city}", translation.WithArgs("Москва"))
	fmt.Println(tr)
	tr = icu.Trans(ctx, "It costs {cost}", translation.WithNumber("cost", 1000.00, arg.WithNumberFormat(arg.NumberFormatDecimal)))
	fmt.Println(tr)
	// Output:
	// Привет Москва
	// Это стоит 1 000,00.
}

Чтобы каждый раз не определять язык можно использовать библиотеку translation, можно передать язык в контексте или использовать указанный в самом методе. Она использует библиотеку text как один из провайдеров, но вы можете подменить его и использовать с другой библиотекой.

Выводы

В golang уже ведется работа над библиотеками позволяющим локализировать приложения. Есть свои минусы, одним из на мое мнение существенных - не поддерживается стандарт icu message, большинство переводов пишут не программисты и есть уже готовые инструменты работающие с данным форматом.

Дополнительные материалы