diff --git a/cmd/web/go.mod b/cmd/web/go.mod index 59c751ee..292bdb0c 100644 --- a/cmd/web/go.mod +++ b/cmd/web/go.mod @@ -35,11 +35,13 @@ require ( github.com/issue9/mux/v7 v7.4.1 // indirect github.com/issue9/scheduled v0.19.3 // indirect github.com/issue9/sliceutil v0.15.1 // indirect + github.com/jellydator/ttlcache/v3 v3.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.17.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect ) diff --git a/cmd/web/go.sum b/cmd/web/go.sum index cd4fe7b8..ca13d7a5 100644 --- a/cmd/web/go.sum +++ b/cmd/web/go.sum @@ -50,6 +50,8 @@ github.com/issue9/unique/v2 v2.0.1 h1:Tdbq7hWZd7rvnf3ckUqzvEftOBWl1Z3S60Jk/zbPvy github.com/issue9/unique/v2 v2.0.1/go.mod h1:oYIXt0BXX4tekc9+77oBu/ROsm7tr5kmD78XEqFoWwk= github.com/issue9/version v1.0.8 h1:IsNdDYdV8UGDGwwgp8H4RszJE0Ko26HjWg9pZzyOivs= github.com/issue9/version v1.0.8/go.mod h1:w8bQwODBOG5+iaS3qIJbElxxpp3Uo4x5F39qKBqwpdc= +github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= +github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= @@ -72,6 +74,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= diff --git a/go.mod b/go.mod index acd7ae41..44edf84c 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/issue9/web +go 1.21 + require ( github.com/andybalholm/brotli v1.1.0 github.com/issue9/assert/v4 v4.1.1 @@ -16,6 +18,7 @@ require ( github.com/issue9/source v0.8.2 github.com/issue9/term/v3 v3.2.7 github.com/issue9/unique/v2 v2.0.1 + github.com/jellydator/ttlcache/v3 v3.2.0 github.com/klauspost/compress v1.17.7 golang.org/x/crypto v0.21.0 golang.org/x/text v0.14.0 @@ -31,7 +34,6 @@ require ( github.com/redis/go-redis/v9 v9.5.1 // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/net v0.21.0 // indirect + golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.18.0 // indirect ) - -go 1.21 diff --git a/go.sum b/go.sum index 9114c0ab..d07e37c4 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/issue9/assert/v4 v4.1.1 h1:OhPE8SB8n/qZCNGLQa+6MQtr/B3oON0JAVj68k8jJlc= @@ -40,16 +42,26 @@ github.com/issue9/term/v3 v3.2.7 h1:esfhoinbQ65P3oFscXhticrDFOgZJQqUwL/IC70HiWc= github.com/issue9/term/v3 v3.2.7/go.mod h1:DvA/fPiKzX11P/ZoVWJG5QMVpI0ia+uqiU31iZV2jHE= github.com/issue9/unique/v2 v2.0.1 h1:Tdbq7hWZd7rvnf3ckUqzvEftOBWl1Z3S60Jk/zbPvyM= github.com/issue9/unique/v2 v2.0.1/go.mod h1:oYIXt0BXX4tekc9+77oBu/ROsm7tr5kmD78XEqFoWwk= +github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE= +github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/internal/locale/bench_test.go b/internal/locale/bench_test.go index 57bb300d..696a0528 100644 --- a/internal/locale/bench_test.go +++ b/internal/locale/bench_test.go @@ -13,17 +13,32 @@ import ( func BenchmarkLocale_NewPrinter(b *testing.B) { l := New(language.SimplifiedChinese, nil, nil) - b.Run("equal id", func(b *testing.B) { + b.Run("equal Locale.id", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { l.NewPrinter(language.SimplifiedChinese) } }) - b.Run("not equal id", func(b *testing.B) { + b.Run("not equal Locale.id", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { l.NewPrinter(language.TraditionalChinese) } }) + + langs := []language.Tag{ + language.Chinese, + language.SimplifiedChinese, + language.TraditionalChinese, + language.MustParse("zh-CN"), + language.MustParse("cmn-Hans"), + } + b.Run("rand id", func(b *testing.B) { + b.ResetTimer() + size := len(langs) + for i := 0; i < b.N; i++ { + l.NewPrinter(langs[i%size]) + } + }) } diff --git a/internal/locale/locale.go b/internal/locale/locale.go index cd242200..55dfd390 100644 --- a/internal/locale/locale.go +++ b/internal/locale/locale.go @@ -10,6 +10,7 @@ import ( "github.com/issue9/config" "github.com/issue9/localeutil/message/serialize" + "github.com/jellydator/ttlcache/v3" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/message/catalog" @@ -21,6 +22,7 @@ type Locale struct { id language.Tag config *config.Config printer *message.Printer + ttl *ttlcache.Cache[language.Tag, *message.Printer] } func New(id language.Tag, conf *config.Config, b *catalog.Builder) *Locale { @@ -34,11 +36,13 @@ func New(id language.Tag, conf *config.Config, b *catalog.Builder) *Locale { panic(err) } + p, _ := NewPrinter(id, b) return &Locale{ Builder: b, id: id, config: conf, - printer: NewPrinter(id, b), + printer: p, + ttl: ttlcache.New(ttlcache.WithCapacity[language.Tag, *message.Printer](10)), } } @@ -59,14 +63,20 @@ func (l *Locale) NewPrinter(id language.Tag) *message.Printer { return l.Printer() } - // TODO 以使用频次或是 TTL 的方式缓存常用的 Printer,可以在一定程序上提升 NewPrinter 的性能。 - return NewPrinter(id, l) + if item := l.ttl.Get(id); item != nil { + return item.Value() + } + p, exact := NewPrinter(id, l) + if exact { + l.ttl.Set(id, p, ttlcache.DefaultTTL) + } + return p } // NewPrinter 从 cat 是查找最符合 tag 的语言 ID 并返回对应的 [message.Printer] 对象 -func NewPrinter(tag language.Tag, cat catalog.Catalog) *message.Printer { - tag, _, _ = cat.Matcher().Match(tag) // 从 cat 中查找最合适的 tag - return message.NewPrinter(tag, message.Catalog(cat)) +func NewPrinter(tag language.Tag, cat catalog.Catalog) (*message.Printer, bool) { + tag, _, confidence := cat.Matcher().Match(tag) // 从 cat 中查找最合适的 tag + return message.NewPrinter(tag, message.Catalog(cat)), confidence == language.Exact } func Load(s config.Serializer, b *catalog.Builder, glob string, fsys ...fs.FS) error { diff --git a/internal/locale/locale_test.go b/internal/locale/locale_test.go index 65eabf71..26186dfb 100644 --- a/internal/locale/locale_test.go +++ b/internal/locale/locale_test.go @@ -21,20 +21,20 @@ func TestLocale_Printer(t *testing.T) { a := assert.New(t, false) b := catalog.NewBuilder() - a.NotError(b.SetString(language.SimplifiedChinese, "lang", "hans")) l := New(language.SimplifiedChinese, nil, b) - a.NotNil(l).Equal(l.Sprintf("lang"), "hans") - a.NotError(l.SetString(language.SimplifiedChinese, "lang", "hans-2")) - a.Equal(l.Sprintf("lang"), "hans-2") + a.NotError(l.SetString(language.SimplifiedChinese, "lang", "hans")). + NotNil(l).Equal(l.Sprintf("lang"), "hans"). + NotError(l.SetString(language.SimplifiedChinese, "lang", "hans-2")). + Equal(l.Sprintf("lang"), "hans-2") // ID 不存在于 catalog b = catalog.NewBuilder() a.NotError(b.SetString(language.SimplifiedChinese, "lang", "hans")) l = New(language.Afrikaans, nil, b) - a.NotNil(l).Equal(l.Sprintf("lang"), "lang") // 找不到对应的翻译项,返回原值 - a.NotError(l.SetString(language.Afrikaans, "lang", "afrik")) - a.Equal(l.Sprintf("lang"), "afrik") + a.NotNil(l).Equal(l.Sprintf("lang"), "lang"). // 找不到对应的翻译项,返回原值 + NotError(l.SetString(language.Afrikaans, "lang", "afrik")). + Equal(l.Sprintf("lang"), "afrik") } func TestLocale_NewPrinter(t *testing.T) { @@ -61,12 +61,12 @@ func TestLocale_NewPrinter(t *testing.T) { func TestNewPrinter(t *testing.T) { a := assert.New(t, false) - c := catalog.NewBuilder() - a.NotError(c.SetString(language.MustParse("zh-CN"), "k1", "zh-cn")) - a.NotError(c.SetString(language.MustParse("zh-TW"), "k1", "zh-tw")) + c := catalog.NewBuilder(catalog.Fallback(language.MustParse("zh-TW"))) + a.NotError(c.SetString(language.MustParse("zh-CN"), "k1", "zh-cn")). + NotError(c.SetString(language.MustParse("zh-TW"), "k1", "zh-tw")) - p := NewPrinter(language.MustParse("cmn-hans"), c) - a.Equal(p.Sprintf("k1"), "zh-cn") + p, ok := NewPrinter(language.MustParse("und"), c) + a.Equal(p.Sprintf("k1"), "zh-tw").False(ok) } func Test_Load(t *testing.T) { diff --git a/server/file.go b/server/file.go index d114810d..43c32f7b 100644 --- a/server/file.go +++ b/server/file.go @@ -65,5 +65,6 @@ func NewPrinter(glob string, fsys ...fs.FS) (*localeutil.Printer, error) { return nil, err } - return locale.NewPrinter(tag, b), nil + p, _ := locale.NewPrinter(tag, b) + return p, nil }