diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml new file mode 100644 index 00000000..72ba653a --- /dev/null +++ b/.github/workflows/l10n.yml @@ -0,0 +1,28 @@ +name: L10n +on: + push: + branches: + - main + pull_request: +jobs: + mockery: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + cache: true + go-version: 'stable' + - name: Install xgotext + run: | + go install github.com/leonelquinteros/gotext/cli/xgotext@latest + - name: Generate pot files + run: | + ~/go/bin/xgotext -default web -pkg-tree ./cmd/web -out ./pkg/embed/locales + ~/go/bin/xgotext -default cli -pkg-tree ./cmd/cli -out ./pkg/embed/locales + - uses: stefanzweifel/git-auto-commit-action@v5 + name: Commit changes + with: + commit_message: "chore(l10n): update pot files" diff --git a/cmd/cli/wire_gen.go b/cmd/cli/wire_gen.go index c0e1f96e..88e3ec41 100644 --- a/cmd/cli/wire_gen.go +++ b/cmd/cli/wire_gen.go @@ -50,6 +50,10 @@ func initCli() (*app.Cli, error) { if err != nil { return nil, err } + locale, err := bootstrap.NewT(koanf) + if err != nil { + return nil, err + } logger := bootstrap.NewLog(koanf) db, err := bootstrap.NewDB(koanf, logger) if err != nil { @@ -70,7 +74,7 @@ func initCli() (*app.Cli, error) { backupRepo := data.NewBackupRepo(db, settingRepo, websiteRepo) cliService := service.NewCliService(koanf, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo) cli := route.NewCli(cliService) - command := bootstrap.NewCli(cli) + command := bootstrap.NewCli(locale, cli) gormigrate := bootstrap.NewMigrate(db) benchmarkApp := benchmark.NewApp() dockerApp := docker.NewApp() diff --git a/cmd/web/wire_gen.go b/cmd/web/wire_gen.go index 71e85cc9..edab39af 100644 --- a/cmd/web/wire_gen.go +++ b/cmd/web/wire_gen.go @@ -153,7 +153,7 @@ func initWeb() (*app.Web, error) { if err != nil { return nil, err } - validation := bootstrap.NewValidator(db) + validation := bootstrap.NewValidator(koanf, db) web := app.NewWeb(koanf, mux, server, gormigrate, cron, queue, validation) return web, nil } diff --git a/go.mod b/go.mod index 43795fde..4685a73d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/knadh/koanf/parsers/yaml v0.1.0 github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/v2 v2.1.2 + github.com/leonelquinteros/gotext v1.7.1 github.com/lib/pq v1.10.9 github.com/libdns/alidns v1.0.4 github.com/libdns/cloudflare v0.1.3 diff --git a/go.sum b/go.sum index 2c00328c..08d0c265 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,7 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/wire v0.6.0 h1:HBkoIh4BdSxoyo9PveV8giw7ZsaBOvzWKfcg/6MrVwI= github.com/google/wire v0.6.0/go.mod h1:F4QhpQ9EDIdJ1Mbop/NZBRB+5yrR6qg3BnctaoUk6NA= @@ -92,6 +93,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8= +github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libdns/alidns v1.0.4 h1:Rc3Yy2SzMoho+3q3+fNy9vOVr2h9dcL8OLTgNKgxYbU= @@ -200,6 +203,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= +golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -259,6 +264,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index e10e2785..41e134a5 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -3,4 +3,4 @@ package bootstrap import "github.com/google/wire" // ProviderSet is bootstrap providers. -var ProviderSet = wire.NewSet(NewConf, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue) +var ProviderSet = wire.NewSet(NewConf, NewT, NewLog, NewCli, NewValidator, NewRouter, NewHttp, NewDB, NewMigrate, NewLoader, NewSession, NewCron, NewQueue) diff --git a/internal/bootstrap/cli.go b/internal/bootstrap/cli.go index 30886bb0..2d607a0f 100644 --- a/internal/bootstrap/cli.go +++ b/internal/bootstrap/cli.go @@ -3,39 +3,40 @@ package bootstrap import ( "strings" + "github.com/leonelquinteros/gotext" "github.com/urfave/cli/v3" "github.com/tnb-labs/panel/internal/app" "github.com/tnb-labs/panel/internal/route" ) -func NewCli(cmd *route.Cli) *cli.Command { - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", "名称") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", "用法") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", "版本") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "DESCRIPTION", "描述") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "AUTHOR", "作者") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COMMANDS", "命令") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "GLOBAL OPTIONS", "全局选项") - cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COPYRIGHT", "版权") - cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "NAME", "名称") - cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "USAGE", "用法") - cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "CATEGORY", "分类") - cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "DESCRIPTION", "描述") - cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "OPTIONS", "选项") - cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "NAME", "名称") - cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "USAGE", "用法") - cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "DESCRIPTION", "描述") - cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "COMMANDS", "命令") - cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "OPTIONS", "选项") +func NewCli(t *gotext.Locale, cmd *route.Cli) *cli.Command { + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "NAME", t.Get("NAME")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "USAGE", t.Get("USAGE")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "VERSION", t.Get("VERSION")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "DESCRIPTION", t.Get("DESCRIPTION")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "AUTHOR", t.Get("AUTHOR")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COMMANDS", t.Get("COMMANDS")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "GLOBAL OPTIONS", t.Get("GLOBAL OPTIONS")) + cli.RootCommandHelpTemplate = strings.ReplaceAll(cli.RootCommandHelpTemplate, "COPYRIGHT", t.Get("COPYRIGHT")) + cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "NAME", t.Get("NAME")) + cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "USAGE", t.Get("USAGE")) + cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "CATEGORY", t.Get("CATEGORY")) + cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "DESCRIPTION", t.Get("DESCRIPTION")) + cli.CommandHelpTemplate = strings.ReplaceAll(cli.CommandHelpTemplate, "OPTIONS", t.Get("OPTIONS")) + cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "NAME", t.Get("NAME")) + cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "USAGE", t.Get("USAGE")) + cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "DESCRIPTION", t.Get("USAGE")) + cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "COMMANDS", t.Get("COMMANDS")) + cli.SubcommandHelpTemplate = strings.ReplaceAll(cli.SubcommandHelpTemplate, "OPTIONS", t.Get("OPTIONS")) - cli.RootCommandHelpTemplate += "\n官网:https://panel.haozi.net" - cli.RootCommandHelpTemplate += "\n论坛:https://bbs.haozi.net" - cli.RootCommandHelpTemplate += "\nQ群:12370907\n" + cli.RootCommandHelpTemplate += "\n" + t.Get("Website:https://panel.haozi.net") + cli.RootCommandHelpTemplate += "\n" + t.Get("Forum:https://bbs.haozi.net") + cli.RootCommandHelpTemplate += "\n" + t.Get("QQ Group:12370907") + "\n" return &cli.Command{ Name: "panel-cli", - Usage: "耗子面板命令行工具", + Usage: t.Get("Rat Panel CLI Tool"), Version: app.Version, Commands: cmd.Commands(), } diff --git a/internal/bootstrap/conf.go b/internal/bootstrap/conf.go index 454410f2..052f3dd5 100644 --- a/internal/bootstrap/conf.go +++ b/internal/bootstrap/conf.go @@ -30,7 +30,7 @@ func NewConf() (*koanf.Koanf, error) { func initGlobal(conf *koanf.Koanf) { app.Key = conf.MustString("app.key") if len(app.Key) != 32 { - log.Fatalf("app key must be 32 characters") + log.Fatalf("panel app key must be 32 characters") } app.Root = conf.MustString("app.root") diff --git a/internal/bootstrap/t.go b/internal/bootstrap/t.go new file mode 100644 index 00000000..f85d379c --- /dev/null +++ b/internal/bootstrap/t.go @@ -0,0 +1,35 @@ +package bootstrap + +import ( + "fmt" + "slices" + + "github.com/knadh/koanf/v2" + "github.com/leonelquinteros/gotext" + "github.com/tnb-labs/panel/pkg/embed" +) + +func NewT(conf *koanf.Koanf) (*gotext.Locale, error) { + dir, err := embed.LocalesFS.ReadDir("locales") + if err != nil { + return nil, err + } + var locales []string + for _, d := range dir { + if d.IsDir() { + locales = append(locales, d.Name()) + } + } + + locale := conf.String("app.locale") + if !slices.Contains(locales, locale) { + return nil, fmt.Errorf("failed to load locale %s, available locales: %v", locale, locales) + } + + l := gotext.NewLocaleFSWithPath(locale, embed.LocalesFS, "locales") + + l.AddDomain("web") + l.AddDomain("cli") + + return l, nil +} diff --git a/internal/bootstrap/validator.go b/internal/bootstrap/validator.go index d4731822..df454b13 100644 --- a/internal/bootstrap/validator.go +++ b/internal/bootstrap/validator.go @@ -2,15 +2,24 @@ package bootstrap import ( "github.com/gookit/validate" + "github.com/gookit/validate/locales/ruru" "github.com/gookit/validate/locales/zhcn" + "github.com/gookit/validate/locales/zhtw" + "github.com/knadh/koanf/v2" "gorm.io/gorm" "github.com/tnb-labs/panel/internal/http/rule" ) // NewValidator just for register global rules -func NewValidator(db *gorm.DB) *validate.Validation { - zhcn.RegisterGlobal() +func NewValidator(conf *koanf.Koanf, db *gorm.DB) *validate.Validation { + if conf.String("app.locale") == "zh_CN" { + zhcn.RegisterGlobal() + } else if conf.String("app.locale") == "zh_TW" { + zhtw.RegisterGlobal() + } else if conf.String("app.locale") == "ru_RU" { + ruru.RegisterGlobal() + } validate.Config(func(opt *validate.GlobalOption) { opt.StopOnError = false opt.SkipOnEmpty = true diff --git a/internal/service/file.go b/internal/service/file.go index da4a18d7..86fcc933 100644 --- a/internal/service/file.go +++ b/internal/service/file.go @@ -1,4 +1,4 @@ -//go:build linux +//go:build !windows package service diff --git a/internal/service/file_windows.go b/internal/service/file_windows.go index 50a4a8ee..31a95d9b 100644 --- a/internal/service/file_windows.go +++ b/internal/service/file_windows.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build windows // 这个文件只是为了在 Windows 下能编译通过,实际上并没有任何卵用 diff --git a/pkg/embed/locales/cli.pot b/pkg/embed/locales/cli.pot new file mode 100644 index 00000000..cc7f7659 --- /dev/null +++ b/pkg/embed/locales/cli.pot @@ -0,0 +1,72 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Generator: xgotext\n" + +#: internal/bootstrap/cli.go:18 +msgid "AUTHOR" +msgstr "" + +#: internal/bootstrap/cli.go:24 +msgid "CATEGORY" +msgstr "" + +#: internal/bootstrap/cli.go:19 +#: internal/bootstrap/cli.go:30 +msgid "COMMANDS" +msgstr "" + +#: internal/bootstrap/cli.go:21 +msgid "COPYRIGHT" +msgstr "" + +#: internal/bootstrap/cli.go:17 +#: internal/bootstrap/cli.go:25 +msgid "DESCRIPTION" +msgstr "" + +#: internal/bootstrap/cli.go:34 +msgid "Forum:https://bbs.haozi.net" +msgstr "" + +#: internal/bootstrap/cli.go:20 +msgid "GLOBAL OPTIONS" +msgstr "" + +#: internal/bootstrap/cli.go:14 +#: internal/bootstrap/cli.go:22 +#: internal/bootstrap/cli.go:27 +msgid "NAME" +msgstr "" + +#: internal/bootstrap/cli.go:26 +#: internal/bootstrap/cli.go:31 +msgid "OPTIONS" +msgstr "" + +#: internal/bootstrap/cli.go:35 +msgid "QQ Group:12370907" +msgstr "" + +#: internal/bootstrap/cli.go:39 +msgid "Rat Panel CLI Tool" +msgstr "" + +#: internal/bootstrap/cli.go:15 +#: internal/bootstrap/cli.go:23 +#: internal/bootstrap/cli.go:28 +#: internal/bootstrap/cli.go:29 +msgid "USAGE" +msgstr "" + +#: internal/bootstrap/cli.go:16 +msgid "VERSION" +msgstr "" + +#: internal/bootstrap/cli.go:33 +msgid "Website:https://panel.haozi.net" +msgstr "" \ No newline at end of file diff --git a/pkg/embed/locales/web.pot b/pkg/embed/locales/web.pot new file mode 100644 index 00000000..cc7f7659 --- /dev/null +++ b/pkg/embed/locales/web.pot @@ -0,0 +1,72 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Generator: xgotext\n" + +#: internal/bootstrap/cli.go:18 +msgid "AUTHOR" +msgstr "" + +#: internal/bootstrap/cli.go:24 +msgid "CATEGORY" +msgstr "" + +#: internal/bootstrap/cli.go:19 +#: internal/bootstrap/cli.go:30 +msgid "COMMANDS" +msgstr "" + +#: internal/bootstrap/cli.go:21 +msgid "COPYRIGHT" +msgstr "" + +#: internal/bootstrap/cli.go:17 +#: internal/bootstrap/cli.go:25 +msgid "DESCRIPTION" +msgstr "" + +#: internal/bootstrap/cli.go:34 +msgid "Forum:https://bbs.haozi.net" +msgstr "" + +#: internal/bootstrap/cli.go:20 +msgid "GLOBAL OPTIONS" +msgstr "" + +#: internal/bootstrap/cli.go:14 +#: internal/bootstrap/cli.go:22 +#: internal/bootstrap/cli.go:27 +msgid "NAME" +msgstr "" + +#: internal/bootstrap/cli.go:26 +#: internal/bootstrap/cli.go:31 +msgid "OPTIONS" +msgstr "" + +#: internal/bootstrap/cli.go:35 +msgid "QQ Group:12370907" +msgstr "" + +#: internal/bootstrap/cli.go:39 +msgid "Rat Panel CLI Tool" +msgstr "" + +#: internal/bootstrap/cli.go:15 +#: internal/bootstrap/cli.go:23 +#: internal/bootstrap/cli.go:28 +#: internal/bootstrap/cli.go:29 +msgid "USAGE" +msgstr "" + +#: internal/bootstrap/cli.go:16 +msgid "VERSION" +msgstr "" + +#: internal/bootstrap/cli.go:33 +msgid "Website:https://panel.haozi.net" +msgstr "" \ No newline at end of file