localization
This commit is contained in:
parent
e11f74fac4
commit
e6949e89ed
7
go.mod
7
go.mod
@ -10,14 +10,17 @@ require (
|
|||||||
github.com/jessevdk/go-flags v1.5.0
|
github.com/jessevdk/go-flags v1.5.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mymmrac/telego v0.29.2
|
github.com/mymmrac/telego v0.29.2
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
|
golang.org/x/text v0.14.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
gorm.io/datatypes v1.2.0
|
gorm.io/datatypes v1.2.0
|
||||||
gorm.io/driver/postgres v1.5.0
|
gorm.io/driver/postgres v1.5.0
|
||||||
gorm.io/gorm v1.25.10
|
gorm.io/gorm v1.25.10
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/bytedance/sonic v1.11.3 // indirect
|
github.com/bytedance/sonic v1.11.3 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
@ -47,7 +50,5 @@ require (
|
|||||||
golang.org/x/crypto v0.20.0 // indirect
|
golang.org/x/crypto v0.20.0 // indirect
|
||||||
golang.org/x/sync v0.5.0 // indirect
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
golang.org/x/sys v0.17.0 // indirect
|
golang.org/x/sys v0.17.0 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
gorm.io/driver/mysql v1.4.7 // indirect
|
gorm.io/driver/mysql v1.4.7 // indirect
|
||||||
)
|
)
|
||||||
|
7
go.sum
7
go.sum
@ -1,7 +1,8 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||||
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
|
|
||||||
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
|
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||||
|
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
@ -103,6 +104,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
github.com/mymmrac/telego v0.29.2 h1:5+fQ/b8d8Ld6ihCJ0OLe1CwUdT3t1sIUl3RaSaSvRJs=
|
github.com/mymmrac/telego v0.29.2 h1:5+fQ/b8d8Ld6ihCJ0OLe1CwUdT3t1sIUl3RaSaSvRJs=
|
||||||
github.com/mymmrac/telego v0.29.2/go.mod h1:BsKr+GF9BHqaVaLBwsZeDnfuJcJx2olWuDEtKm4zHMc=
|
github.com/mymmrac/telego v0.29.2/go.mod h1:BsKr+GF9BHqaVaLBwsZeDnfuJcJx2olWuDEtKm4zHMc=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||||
|
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
|
||||||
@ -195,6 +198,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/handler"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/logger"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -58,6 +59,10 @@ func (a *App) DB() *db.Repositories {
|
|||||||
return a.Repositories
|
return a.Repositories
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) Localizer(lang string) locale.Localizer {
|
||||||
|
return locale.For(lang)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) loadConfig() error {
|
func (a *App) loadConfig() error {
|
||||||
config, err := config.LoadConfig()
|
config, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -3,6 +3,7 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/config"
|
||||||
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/db"
|
||||||
|
"gitea.neur0tx.site/Neur0toxine/vegapokerbot/internal/locale"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@ -20,6 +21,7 @@ type App interface {
|
|||||||
TG() *telego.Bot
|
TG() *telego.Bot
|
||||||
Conf() *config.Config
|
Conf() *config.Config
|
||||||
DB() *db.Repositories
|
DB() *db.Repositories
|
||||||
|
Localizer(string) locale.Localizer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler interface {
|
type Handler interface {
|
||||||
|
14
internal/locale/app.en.yaml
Normal file
14
internal/locale/app.en.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
welcome: "Hello! This bot allows you to conduct Scrum Poker directly in a Telegram chat and pass results to issues tied to a Redmine instance. To start, add the bot to the chat where you want to conduct poker. After this, you can continue with the setup."
|
||||||
|
bot_was_added: "Great, the bot has been added to the chat \"{{.Name}}\". Choose chat members who will participate in the poker."
|
||||||
|
choose_at_least_one: "You must choose at least one participant."
|
||||||
|
ask_for_redmine: "Configure integration with Redmine?"
|
||||||
|
please_send_redmine_url: "Please send the URL of your Redmine instance."
|
||||||
|
please_send_redmine_key: "Please send your API key that will be used by the bot to interact with Redmine. The bot will perform the following actions:\n\n• retrieving task data;\n• writing the result of the poker in the task (if configured in subsequent steps).\n\nThe access key for the API can be found on the left part of the page by [this link](/my/account)."
|
||||||
|
invalid_redmine_credentials: "Failed to verify connection with Redmine. Will try setting up again?"
|
||||||
|
redmine_was_connected: "Great, Redmine connected! Now when starting a vote and passing it on to `/poll@vegapokerbot` in the chat, the bot will mention the task theme and generate a link to the task.\n\nThe bot can pass results of the vote directly into custom fields of issues in Redmine. The following data are passed:\n\n• the vote result;\n• the vote result converted to hours (assuming that the vote result is story points, conversion to hours is automatic).\n\nDo you want to send this data to Redmine at the end of the poker?"
|
||||||
|
specify_result_field: "Specify the name of the field where the poker result will be written."
|
||||||
|
should_also_send_hours: "Do you want the poker result to be converted to hours and also sent to Redmine? This is useful if you treat the vote result as story points. In that case the result will be multiplied by 8 before sending it to Redmine. Example: vote result 1.5 means that we will send to Redmine 1.5 * 8 = 12."
|
||||||
|
specify_second_result_field: "Specify the name of the field where the poker result in hours will be writen."
|
||||||
|
setup_done: "Done, now your chat is connected to the bot!\nUse the command`/poll@vegapokerbot` in the connected chat to start a vote."
|
||||||
|
yes: "Yes"
|
||||||
|
no: "No"
|
14
internal/locale/app.ru.yaml
Normal file
14
internal/locale/app.ru.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
welcome: "Привет! Этот бот позволяет проводить Scrum Poker прямо в чате Telegram и передавать результаты в задачи в привязанном Redmine. Для начала добавьте бота в чат, в котором вы хотите проводить poker. После этого вы сможете продолжить настройку."
|
||||||
|
bot_was_added: "Отлично, бот был добавлен в чат \"{{.Name}}\". Выберите участников чата, которые будут участвовать в покере."
|
||||||
|
choose_at_least_one: "Вы должны выбрать хотя бы одного участника."
|
||||||
|
ask_for_redmine: "Настроить интеграцию с Redmine?"
|
||||||
|
please_send_redmine_url: "Пожалуйста, отправьте ссылку на свой инстанс Redmine."
|
||||||
|
please_send_redmine_key: "Отправьте свой API-ключ, который будет использоваться ботом для взаимодействия с Redmine. Бот будет выполнять следующие действия:\n\n• получение данных задачи;\n• запись результата покера в задачу (если будет настроено в следующих шагах).\n\nКлюч доступа к API можно найти в левой части страницы по [этой ссылке](/my/account)."
|
||||||
|
invalid_redmine_credentials: "Не удалось проверить связь с Redmine. Будете пробовать настроить заново?"
|
||||||
|
redmine_was_connected: "Отлично, Redmine подключен! Теперь при начале голосования и передаче в команду `/poll@vegapokerbot` номера задачи бот укажет в сообщении тему задачи и сгенерирует ссылку на задачу.\n\nБот может передавать результаты голосования напрямую в кастомные поля задачи в Redmine. Передаются следующие данные:\n\n• полученная в итоге голосования оценка;\n• конвертированная в часы оценка (если оценка в story points, конвертация в часы автоматическая).\n\nБудете настраивать передачу этих данных?"
|
||||||
|
specify_result_field: "Укажите название поля, в которое будет передаваться результат оценки."
|
||||||
|
should_also_send_hours: "Передавать в другое поле сконвертированный в часы результат оценки? Предполагается, что оценка - это story points, поэтому будет передаваться умноженный на 8 результат. Пример: результат голосования 1,5 - в дополнительное поле передастся 1,5 * 8 = 12."
|
||||||
|
specify_second_result_field: "Укажите название поля, в которое будет передаваться результат оценки в часах."
|
||||||
|
setup_done: "Готово, чат подключен к боту!\nИспользуйте команду`/poll@vegapokerbot` в подключенном чате для запуска голосования."
|
||||||
|
yes: "Да"
|
||||||
|
no: "Нет"
|
66
internal/locale/load.go
Normal file
66
internal/locale/load.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package locale
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed *.yaml
|
||||||
|
var locales embed.FS
|
||||||
|
|
||||||
|
var localizers = map[string]Localizer{}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
entries, err := locales.ReadDir(".")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := []language.Tag{}
|
||||||
|
bundle := i18n.NewBundle(language.English)
|
||||||
|
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
_, err := bundle.LoadMessageFileFS(locales, entry.Name())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
tags = append(tags, extractTagFromPath(entry.Name()))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range tags {
|
||||||
|
localizers[tag.String()] = &localizer{loc: i18n.NewLocalizer(bundle, tag.String())}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTagFromPath(path string) language.Tag {
|
||||||
|
var langTag string
|
||||||
|
formatStartIdx := -1
|
||||||
|
for i := len(path) - 1; i >= 0; i-- {
|
||||||
|
c := path[i]
|
||||||
|
if os.IsPathSeparator(c) {
|
||||||
|
if formatStartIdx != -1 {
|
||||||
|
langTag = path[i+1 : formatStartIdx]
|
||||||
|
}
|
||||||
|
return language.Make(langTag)
|
||||||
|
}
|
||||||
|
if path[i] == '.' {
|
||||||
|
if formatStartIdx != -1 {
|
||||||
|
langTag = path[i+1 : formatStartIdx]
|
||||||
|
return language.Make(langTag)
|
||||||
|
}
|
||||||
|
formatStartIdx = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if formatStartIdx != -1 {
|
||||||
|
return language.Make(path[:formatStartIdx])
|
||||||
|
}
|
||||||
|
return language.English
|
||||||
|
}
|
||||||
|
|
||||||
|
func For(lang string) Localizer {
|
||||||
|
return localizers[lang]
|
||||||
|
}
|
23
internal/locale/localizer.go
Normal file
23
internal/locale/localizer.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package locale
|
||||||
|
|
||||||
|
import "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
|
||||||
|
type Localizer interface {
|
||||||
|
Message(string) string
|
||||||
|
Template(string, map[string]interface{}) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type localizer struct {
|
||||||
|
loc *i18n.Localizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localizer) Message(str string) string {
|
||||||
|
return l.Template(str, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localizer) Template(str string, tpl map[string]interface{}) string {
|
||||||
|
return l.loc.MustLocalize(&i18n.LocalizeConfig{
|
||||||
|
MessageID: str,
|
||||||
|
TemplateData: tpl,
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user