diff --git a/go.mod b/go.mod index 2d215dd..7b8900d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.24.0 require ( github.com/appleboy/gin-jwt/v2 v2.10.2 - github.com/chai2010/gettext-go v1.0.3 github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 github.com/gin-contrib/pprof v1.5.2 github.com/gin-gonic/gin v1.10.0 @@ -18,6 +17,7 @@ require ( github.com/knadh/koanf/providers/env v1.0.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/libdns/cloudflare v0.1.3 github.com/libdns/he v1.0.2 github.com/libdns/libdns v0.2.3 @@ -29,6 +29,7 @@ require ( github.com/robfig/cron/v3 v3.0.1 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.4 github.com/tidwall/gjson v1.18.0 golang.org/x/crypto v0.36.0 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 @@ -74,7 +75,6 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/swaggo/swag v1.16.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect diff --git a/go.sum b/go.sum index 03d06a5..320dde0 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,6 @@ github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= -github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -98,6 +96,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/leonelquinteros/gotext v1.7.1 h1:/JNPeE3lY5JeVYv2+KBpz39994W3W9fmZCGq3eO9Ri8= +github.com/leonelquinteros/gotext v1.7.1/go.mod h1:I0WoFDn9u2D3VbPnnDPT8mzZu0iSXG8iih+AH2fHHqg= github.com/libdns/cloudflare v0.1.3 h1:XPFa2f3Mm/3FDNwl9Ki2bfAQJ0Cm5GQB0e8PQVy25Us= github.com/libdns/cloudflare v0.1.3/go.mod h1:XbvSCSMcxspwpSialM3bq0LsS3/Houy9WYxW8Ok8b6M= github.com/libdns/he v1.0.2 h1:AUlHRkRyVCsoaifIXZRoH9dn+nj0MXXwLMvPV4OlNdk= diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index a7eacdc..d564fc7 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -3,29 +3,45 @@ package i18n import ( "embed" "fmt" + "io/fs" + "path" "sync" - "github.com/chai2010/gettext-go" + "github.com/leonelquinteros/gotext" ) //go:embed translations var Translations embed.FS type Localizer struct { - intlMap map[string]gettext.Gettexter + intlMap map[string]gotext.Translator lang string + domain string + path string + fs fs.FS mu sync.RWMutex } -func NewLocalizer(lang, domain, path string, data any) *Localizer { - intl := gettext.New(domain, path, data) - intl.SetLanguage(lang) +func NewLocalizer(lang, domain, path string, fs fs.FS) *Localizer { + loc := &Localizer{ + intlMap: make(map[string]gotext.Translator), + lang: lang, + domain: domain, + path: path, + fs: fs, + } - intlMap := make(map[string]gettext.Gettexter) - intlMap[lang] = intl + file := loc.findExt(lang, "mo") + if file == "" { + return loc + } - return &Localizer{intlMap: intlMap, lang: lang} + mo := gotext.NewMoFS(loc.fs) + mo.ParseFile(file) + loc.intlMap[lang] = mo + + return loc } func (l *Localizer) SetLanguage(lang string) { @@ -45,14 +61,19 @@ func (l *Localizer) Exists(lang string) bool { return false } -func (l *Localizer) AppendIntl(lang, domain, path string, data any) { - intl := gettext.New(domain, path, data) - intl.SetLanguage(lang) +func (l *Localizer) AppendIntl(lang string) { + file := l.findExt(lang, "mo") + if file == "" { + return + } + + mo := gotext.NewMoFS(l.fs) + mo.ParseFile(file) l.mu.Lock() defer l.mu.Unlock() - l.intlMap[lang] = intl + l.intlMap[lang] = mo } // Modified from k8s.io/kubectl/pkg/util/i18n @@ -65,7 +86,7 @@ func (l *Localizer) T(orig string) string { return orig } - return intl.PGettext("", orig) + return intl.Get(orig) } // N translates a string, possibly substituting arguments into it along @@ -80,9 +101,9 @@ func (l *Localizer) N(orig string, args ...int) string { } if len(args) == 0 { - return intl.PGettext("", orig) + return intl.Get(orig) } - return fmt.Sprintf(intl.PNGettext("", orig, orig+".plural", args[0]), + return fmt.Sprintf(intl.GetN(orig, orig+".plural", args[0]), args[0]) } @@ -96,3 +117,37 @@ func (l *Localizer) ErrorT(defaultValue string, args ...any) error { func (l *Localizer) Tf(defaultValue string, args ...any) string { return fmt.Sprintf(l.T(defaultValue), args...) } + +// https://github.com/leonelquinteros/gotext/blob/v1.7.1/locale.go +func (l *Localizer) findExt(lang, ext string) string { + filename := path.Join(l.path, lang, "LC_MESSAGES", l.domain+"."+ext) + if l.fileExists(filename) { + return filename + } + + if len(lang) > 2 { + filename = path.Join(l.path, lang[:2], "LC_MESSAGES", l.domain+"."+ext) + if l.fileExists(filename) { + return filename + } + } + + filename = path.Join(l.path, lang, l.domain+"."+ext) + if l.fileExists(filename) { + return filename + } + + if len(lang) > 2 { + filename = path.Join(l.path, lang[:2], l.domain+"."+ext) + if l.fileExists(filename) { + return filename + } + } + + return "" +} + +func (l *Localizer) fileExists(filename string) bool { + _, err := fs.Stat(l.fs, filename) + return err == nil +} diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go new file mode 100644 index 0000000..912f47d --- /dev/null +++ b/pkg/i18n/i18n_test.go @@ -0,0 +1,32 @@ +package i18n + +import ( + "testing" +) + +func TestI18n(t *testing.T) { + const testStr = "database error" + + t.Run("SwitchLocale", func(t *testing.T) { + loc := NewLocalizer("zh_CN", "nezha", "translations", Translations) + translated := loc.T(testStr) + if translated != "数据库错误" { + t.Fatalf("expected %s, but got %s", "数据库错误", translated) + } + + loc.AppendIntl("zh_TW") + loc.SetLanguage("zh_TW") + translated = loc.T(testStr) + if translated != "資料庫錯誤" { + t.Fatalf("expected %s, but got %s", "資料庫錯誤", translated) + } + }) + + t.Run("Fallback", func(t *testing.T) { + loc := NewLocalizer("invalid", "nezha", "translations", Translations) + fallbackStr := loc.T(testStr) + if fallbackStr != testStr { + t.Fatalf("expected %s, but got %s", testStr, fallbackStr) + } + }) +} diff --git a/service/singleton/i18n.go b/service/singleton/i18n.go index 1adca0e..7cff74f 100644 --- a/service/singleton/i18n.go +++ b/service/singleton/i18n.go @@ -1,9 +1,6 @@ package singleton import ( - "archive/zip" - "bytes" - "fmt" "log" "strings" @@ -27,12 +24,7 @@ func loadTranslation() error { } lang = strings.Replace(lang, "-", "_", 1) - data, err := getTranslationArchive(lang) - if err != nil { - return err - } - - Localizer = i18n.NewLocalizer(lang, domain, domain+".zip", data) + Localizer = i18n.NewLocalizer(lang, domain, "translations", i18n.Translations) return nil } @@ -43,41 +35,7 @@ func OnUpdateLang(lang string) error { return nil } - data, err := getTranslationArchive(lang) - if err != nil { - return err - } - - Localizer.AppendIntl(lang, domain, domain+".zip", data) + Localizer.AppendIntl(lang) Localizer.SetLanguage(lang) return nil } - -func getTranslationArchive(lang string) ([]byte, error) { - files := [...]string{ - fmt.Sprintf("translations/%s/LC_MESSAGES/%s.po", lang, domain), - fmt.Sprintf("translations/%s/LC_MESSAGES/%s.mo", lang, domain), - } - - buf := new(bytes.Buffer) - w := zip.NewWriter(buf) - - for _, file := range files { - f, err := w.Create(file) - if err != nil { - return nil, err - } - data, err := i18n.Translations.ReadFile(file) - if err != nil { - return nil, err - } - if _, err := f.Write(data); err != nil { - return nil, err - } - } - if err := w.Close(); err != nil { - return nil, err - } - - return buf.Bytes(), nil -}