i18n: replace gettext implementation (#1056)

This commit is contained in:
UUBulb
2025-04-13 12:26:03 +08:00
committed by GitHub
parent 663688ea94
commit 91cb5e903f
5 changed files with 108 additions and 63 deletions

4
go.mod
View File

@@ -4,7 +4,6 @@ go 1.24.0
require ( require (
github.com/appleboy/gin-jwt/v2 v2.10.2 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/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
github.com/gin-contrib/pprof v1.5.2 github.com/gin-contrib/pprof v1.5.2
github.com/gin-gonic/gin v1.10.0 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/env v1.0.0
github.com/knadh/koanf/providers/file v1.1.2 github.com/knadh/koanf/providers/file v1.1.2
github.com/knadh/koanf/v2 v2.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/cloudflare v0.1.3
github.com/libdns/he v1.0.2 github.com/libdns/he v1.0.2
github.com/libdns/libdns v0.2.3 github.com/libdns/libdns v0.2.3
@@ -29,6 +29,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 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/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect

4
go.sum
View File

@@ -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.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 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 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 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 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= 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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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 h1:XPFa2f3Mm/3FDNwl9Ki2bfAQJ0Cm5GQB0e8PQVy25Us=
github.com/libdns/cloudflare v0.1.3/go.mod h1:XbvSCSMcxspwpSialM3bq0LsS3/Houy9WYxW8Ok8b6M= github.com/libdns/cloudflare v0.1.3/go.mod h1:XbvSCSMcxspwpSialM3bq0LsS3/Houy9WYxW8Ok8b6M=
github.com/libdns/he v1.0.2 h1:AUlHRkRyVCsoaifIXZRoH9dn+nj0MXXwLMvPV4OlNdk= github.com/libdns/he v1.0.2 h1:AUlHRkRyVCsoaifIXZRoH9dn+nj0MXXwLMvPV4OlNdk=

View File

@@ -3,29 +3,45 @@ package i18n
import ( import (
"embed" "embed"
"fmt" "fmt"
"io/fs"
"path"
"sync" "sync"
"github.com/chai2010/gettext-go" "github.com/leonelquinteros/gotext"
) )
//go:embed translations //go:embed translations
var Translations embed.FS var Translations embed.FS
type Localizer struct { type Localizer struct {
intlMap map[string]gettext.Gettexter intlMap map[string]gotext.Translator
lang string lang string
domain string
path string
fs fs.FS
mu sync.RWMutex mu sync.RWMutex
} }
func NewLocalizer(lang, domain, path string, data any) *Localizer { func NewLocalizer(lang, domain, path string, fs fs.FS) *Localizer {
intl := gettext.New(domain, path, data) loc := &Localizer{
intl.SetLanguage(lang) intlMap: make(map[string]gotext.Translator),
lang: lang,
domain: domain,
path: path,
fs: fs,
}
intlMap := make(map[string]gettext.Gettexter) file := loc.findExt(lang, "mo")
intlMap[lang] = intl 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) { func (l *Localizer) SetLanguage(lang string) {
@@ -45,14 +61,19 @@ func (l *Localizer) Exists(lang string) bool {
return false return false
} }
func (l *Localizer) AppendIntl(lang, domain, path string, data any) { func (l *Localizer) AppendIntl(lang string) {
intl := gettext.New(domain, path, data) file := l.findExt(lang, "mo")
intl.SetLanguage(lang) if file == "" {
return
}
mo := gotext.NewMoFS(l.fs)
mo.ParseFile(file)
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
l.intlMap[lang] = intl l.intlMap[lang] = mo
} }
// Modified from k8s.io/kubectl/pkg/util/i18n // Modified from k8s.io/kubectl/pkg/util/i18n
@@ -65,7 +86,7 @@ func (l *Localizer) T(orig string) string {
return orig return orig
} }
return intl.PGettext("", orig) return intl.Get(orig)
} }
// N translates a string, possibly substituting arguments into it along // 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 { 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]) args[0])
} }
@@ -96,3 +117,37 @@ func (l *Localizer) ErrorT(defaultValue string, args ...any) error {
func (l *Localizer) Tf(defaultValue string, args ...any) string { func (l *Localizer) Tf(defaultValue string, args ...any) string {
return fmt.Sprintf(l.T(defaultValue), args...) 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
}

32
pkg/i18n/i18n_test.go Normal file
View File

@@ -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)
}
})
}

View File

@@ -1,9 +1,6 @@
package singleton package singleton
import ( import (
"archive/zip"
"bytes"
"fmt"
"log" "log"
"strings" "strings"
@@ -27,12 +24,7 @@ func loadTranslation() error {
} }
lang = strings.Replace(lang, "-", "_", 1) lang = strings.Replace(lang, "-", "_", 1)
data, err := getTranslationArchive(lang) Localizer = i18n.NewLocalizer(lang, domain, "translations", i18n.Translations)
if err != nil {
return err
}
Localizer = i18n.NewLocalizer(lang, domain, domain+".zip", data)
return nil return nil
} }
@@ -43,41 +35,7 @@ func OnUpdateLang(lang string) error {
return nil return nil
} }
data, err := getTranslationArchive(lang) Localizer.AppendIntl(lang)
if err != nil {
return err
}
Localizer.AppendIntl(lang, domain, domain+".zip", data)
Localizer.SetLanguage(lang) Localizer.SetLanguage(lang)
return nil 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
}