From 3cd005911da06cfa51df217c17be3ea0dcbc2988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E4=BA=AE?= <739476267@qq.com> Date: Tue, 10 Oct 2023 20:02:52 +0800 Subject: [PATCH] fix issue reading resource from manage in function New of package gres (#2961) --- i18n/gi18n/gi18n_ctx.go | 8 +- i18n/gi18n/gi18n_manager.go | 124 ++++++++++++------ i18n/gi18n/gi18n_z_unit_test.go | 88 ++++++++++++- i18n/gi18n/testdata/i18n-dir/zh-CN/hello.json | 3 +- i18n/gi18n/testdata/i18n-dir/zh-CN/world.json | 1 + 5 files changed, 174 insertions(+), 50 deletions(-) diff --git a/i18n/gi18n/gi18n_ctx.go b/i18n/gi18n/gi18n_ctx.go index a2c7293a769..8d164ac9e52 100644 --- a/i18n/gi18n/gi18n_ctx.go +++ b/i18n/gi18n/gi18n_ctx.go @@ -7,10 +7,14 @@ // Package gi18n implements internationalization and localization. package gi18n -import "context" +import ( + "context" + + "github.com/gogf/gf/v2/os/gctx" +) const ( - ctxLanguage = "I18nLanguage" + ctxLanguage gctx.StrKey = "I18nLanguage" ) // WithLanguage append language setting to the context and returns a new context. diff --git a/i18n/gi18n/gi18n_manager.go b/i18n/gi18n/gi18n_manager.go index f22d1a2ce00..3fc51e040f1 100644 --- a/i18n/gi18n/gi18n_manager.go +++ b/i18n/gi18n/gi18n_manager.go @@ -23,19 +23,30 @@ import ( "github.com/gogf/gf/v2/util/gconv" ) +// pathType is the type for i18n file path. +type pathType string + +const ( + pathTypeNone pathType = "none" + pathTypeNormal pathType = "normal" + pathTypeGres pathType = "gres" +) + // Manager for i18n contents, it is concurrent safe, supporting hot reload. type Manager struct { - mu sync.RWMutex - data map[string]map[string]string // Translating map. - pattern string // Pattern for regex parsing. - options Options // configuration options. + mu sync.RWMutex + data map[string]map[string]string // Translating map. + pattern string // Pattern for regex parsing. + pathType pathType // Path type for i18n files. + options Options // configuration options. } // Options is used for i18n object configuration. type Options struct { - Path string // I18n files storage path. - Language string // Default local language. - Delimiters []string // Delimiters for variable parsing. + Path string // I18n files storage path. + Language string // Default local language. + Delimiters []string // Delimiters for variable parsing. + Resource *gres.Resource // Resource for i18n files. } var ( @@ -54,10 +65,25 @@ var ( // It uses a default one if it's not passed. func New(options ...Options) *Manager { var opts Options + var pathType = pathTypeNone if len(options) > 0 { opts = options[0] + pathType = opts.checkPathType(opts.Path) } else { - opts = DefaultOptions() + opts = Options{} + for _, folder := range searchFolders { + pathType = opts.checkPathType(folder) + if pathType != pathTypeNone { + break + } + } + if opts.Path != "" { + // To avoid of the source path of GoFrame: github.com/gogf/i18n/gi18n + if gfile.Exists(opts.Path + gfile.Separator + "gi18n") { + opts.Path = "" + pathType = pathTypeNone + } + } } if len(opts.Language) == 0 { opts.Language = defaultLanguage @@ -72,45 +98,47 @@ func New(options ...Options) *Manager { gregex.Quote(opts.Delimiters[0]), gregex.Quote(opts.Delimiters[1]), ), + pathType: pathType, } intlog.Printf(context.TODO(), `New: %#v`, m) return m } -// DefaultOptions creates and returns a default options for i18n manager. -func DefaultOptions() Options { - var path string - for _, folder := range searchFolders { - path, _ = gfile.Search(folder) - if path != "" { - break - } +// checkPathType checks and returns the path type for given directory path. +func (o *Options) checkPathType(dirPath string) pathType { + if dirPath == "" { + return pathTypeNone } - if path != "" { - // To avoid of the source path of GoFrame: github.com/gogf/i18n/gi18n - if gfile.Exists(path + gfile.Separator + "gi18n") { - path = "" - } + + if o.Resource == nil { + o.Resource = gres.Instance() } - return Options{ - Path: path, - Language: "en", - Delimiters: defaultDelimiters, + + if o.Resource.Contains(dirPath) { + o.Path = dirPath + return pathTypeGres } + + realPath, _ := gfile.Search(dirPath) + if realPath != "" { + o.Path = realPath + return pathTypeNormal + } + + return pathTypeNone } // SetPath sets the directory path storing i18n files. func (m *Manager) SetPath(path string) error { - if gres.Contains(path) { - m.options.Path = path - } else { - realPath, _ := gfile.Search(path) - if realPath == "" { - return gerror.NewCodef(gcode.CodeInvalidParameter, `%s does not exist`, path) - } - m.options.Path = realPath + pathType := m.options.checkPathType(path) + if pathType == pathTypeNone { + return gerror.NewCodef(gcode.CodeInvalidParameter, `%s does not exist`, path) } - intlog.Printf(context.TODO(), `SetPath: %s`, m.options.Path) + + m.pathType = pathType + intlog.Printf(context.TODO(), `SetPath[%s]: %s`, m.pathType, m.options.Path) + // Reset the manager after path changed. + m.reset() return nil } @@ -190,6 +218,13 @@ func (m *Manager) GetContent(ctx context.Context, key string) string { return "" } +// reset reset data of the manager. +func (m *Manager) reset() { + m.mu.Lock() + defer m.mu.Unlock() + m.data = nil +} + // init initializes the manager for lazy initialization design. // The i18n manager is only initialized once. func (m *Manager) init(ctx context.Context) { @@ -201,10 +236,17 @@ func (m *Manager) init(ctx context.Context) { } m.mu.RUnlock() + defer func() { + intlog.Printf(ctx, `Manager init finish: %#v`, m) + }() + + intlog.Printf(ctx, `init path: %s`, m.options.Path) + m.mu.Lock() defer m.mu.Unlock() - if gres.Contains(m.options.Path) { - files := gres.ScanDirFile(m.options.Path, "*.*", true) + switch m.pathType { + case pathTypeGres: + files := m.options.Resource.ScanDirFile(m.options.Path, "*.*", true) if len(files) > 0 { var ( path string @@ -234,7 +276,7 @@ func (m *Manager) init(ctx context.Context) { } } } - } else if m.options.Path != "" { + case pathTypeNormal: files, _ := gfile.ScanDirFile(m.options.Path, "*.*", true) if len(files) == 0 { return @@ -264,12 +306,12 @@ func (m *Manager) init(ctx context.Context) { intlog.Errorf(ctx, "load i18n file '%s' failed: %+v", file, err) } } + intlog.Printf(ctx, "i18n files loaded in path: %s", m.options.Path) // Monitor changes of i18n files for hot reload feature. - _, _ = gfsnotify.Add(path, func(event *gfsnotify.Event) { + _, _ = gfsnotify.Add(m.options.Path, func(event *gfsnotify.Event) { + intlog.Printf(ctx, `i18n file changed: %s`, event.Path) // Any changes of i18n files, clear the data. - m.mu.Lock() - m.data = nil - m.mu.Unlock() + m.reset() gfsnotify.Exit() }) } diff --git a/i18n/gi18n/gi18n_z_unit_test.go b/i18n/gi18n/gi18n_z_unit_test.go index 1a20c0cff94..5ef6fc86f10 100644 --- a/i18n/gi18n/gi18n_z_unit_test.go +++ b/i18n/gi18n/gi18n_z_unit_test.go @@ -7,8 +7,10 @@ package gi18n_test import ( + "time" + + "github.com/gogf/gf/v2/encoding/gbase64" "github.com/gogf/gf/v2/os/gctx" - _ "github.com/gogf/gf/v2/os/gres/testdata/data" "context" "testing" @@ -120,10 +122,9 @@ func Test_DefaultManager(t *testing.T) { } func Test_Instance(t *testing.T) { - gres.Dump() gtest.C(t, func(t *gtest.T) { m := gi18n.Instance() - err := m.SetPath("i18n-dir") + err := m.SetPath(gtest.DataPath("i18n-dir")) t.AssertNil(err) m.SetLanguage("zh-CN") t.Assert(m.T(context.Background(), "{#hello}{#world}"), "你好世界") @@ -141,7 +142,7 @@ func Test_Instance(t *testing.T) { // Default language is: en gtest.C(t, func(t *gtest.T) { m := gi18n.Instance(gconv.String(gtime.TimestampNano())) - m.SetPath("i18n-dir") + m.SetPath(gtest.DataPath("i18n-dir")) t.Assert(m.T(context.Background(), "{#hello}{#world}"), "HelloWorld") }) } @@ -149,7 +150,7 @@ func Test_Instance(t *testing.T) { func Test_Resource(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := g.I18n("resource") - err := m.SetPath("i18n-dir") + err := m.SetPath(gtest.DataPath("i18n-dir")) t.AssertNil(err) m.SetLanguage("none") @@ -180,8 +181,83 @@ func Test_SetCtxLanguage(t *testing.T) { }) gtest.C(t, func(t *gtest.T) { - ctx := gi18n.WithLanguage(nil, "zh-CN") + ctx := gi18n.WithLanguage(context.Background(), "zh-CN") t.Assert(gi18n.LanguageFromCtx(ctx), "zh-CN") }) } + +func Test_GetContent(t *testing.T) { + i18n := gi18n.New(gi18n.Options{ + Path: gtest.DataPath("i18n-file"), + }) + gtest.C(t, func(t *gtest.T) { + t.Assert(i18n.GetContent(context.Background(), "hello"), "Hello") + + ctx := gi18n.WithLanguage(context.Background(), "zh-CN") + t.Assert(i18n.GetContent(ctx, "hello"), "你好") + + ctx = gi18n.WithLanguage(context.Background(), "unknown") + t.Assert(i18n.GetContent(ctx, "hello"), "") + }) +} + +func Test_PathInResource(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + binContent, err := gres.Pack(gtest.DataPath("i18n")) + t.AssertNil(err) + err = gres.Add(gbase64.EncodeToString(binContent)) + t.AssertNil(err) + + i18n := gi18n.New() + i18n.SetLanguage("zh-CN") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + + err = i18n.SetPath("i18n") + t.Assert(err, nil) + i18n.SetLanguage("ja") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + }) +} + +func Test_PathInNormal(t *testing.T) { + // Copy i18n files to current directory. + gfile.CopyDir(gtest.DataPath("i18n"), gfile.Join(gdebug.CallerDirectory(), "manifest/i18n")) + // Remove copied files after testing. + defer gfile.Remove(gfile.Join(gdebug.CallerDirectory(), "manifest")) + + i18n := gi18n.New() + + gtest.C(t, func(t *gtest.T) { + i18n.SetLanguage("zh-CN") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + // Set not exist path. + err := i18n.SetPath("i18n-not-exist") + t.AssertNE(err, nil) + err = i18n.SetPath("") + t.AssertNE(err, nil) + i18n.SetLanguage("ja") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + }) + + // Change language file content. + gtest.C(t, func(t *gtest.T) { + i18n.SetLanguage("en") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}{#name}"), "HelloWorld{#name}") + err := gfile.PutContentsAppend(gfile.Join(gdebug.CallerDirectory(), "manifest/i18n/en.toml"), "\nname = \"GoFrame\"") + t.Assert(err, nil) + // Wait for the file modification time to change. + time.Sleep(10 * time.Millisecond) + t.Assert(i18n.T(context.Background(), "{#hello}{#world}{#name}"), "HelloWorldGoFrame") + }) + + // Add new language + gtest.C(t, func(t *gtest.T) { + err := gfile.PutContents(gfile.Join(gdebug.CallerDirectory(), "manifest/i18n/en-US.toml"), "lang = \"en-US\"") + t.Assert(err, nil) + // Wait for the file modification time to change. + time.Sleep(10 * time.Millisecond) + i18n.SetLanguage("en-US") + t.Assert(i18n.T(context.Background(), "{#lang}"), "en-US") + }) +} diff --git a/i18n/gi18n/testdata/i18n-dir/zh-CN/hello.json b/i18n/gi18n/testdata/i18n-dir/zh-CN/hello.json index b8eb10e9404..3d534dbf142 100644 --- a/i18n/gi18n/testdata/i18n-dir/zh-CN/hello.json +++ b/i18n/gi18n/testdata/i18n-dir/zh-CN/hello.json @@ -1,3 +1,4 @@ { - "hello": "你好" + "你好": "hello", + "hello": "你好" } \ No newline at end of file diff --git a/i18n/gi18n/testdata/i18n-dir/zh-CN/world.json b/i18n/gi18n/testdata/i18n-dir/zh-CN/world.json index 9d63921498b..540f0011865 100644 --- a/i18n/gi18n/testdata/i18n-dir/zh-CN/world.json +++ b/i18n/gi18n/testdata/i18n-dir/zh-CN/world.json @@ -1,3 +1,4 @@ { + "世界": "world", "world": "世界" } \ No newline at end of file