diff --git a/i18n/gi18n/gi18n.go b/i18n/gi18n/gi18n.go index 09b78f11416..a18f6dcb83b 100644 --- a/i18n/gi18n/gi18n.go +++ b/i18n/gi18n/gi18n.go @@ -7,13 +7,20 @@ // Package gi18n implements internationalization and localization. package gi18n -import "context" +import ( + "context" + "io/fs" +) // SetPath sets the directory path storing i18n files. func SetPath(path string) error { return Instance().SetPath(path) } +func SetPathFS(dirfs fs.FS) error { + return Instance().SetPathFS(dirfs) +} + // SetLanguage sets the language for translator. func SetLanguage(language string) { Instance().SetLanguage(language) diff --git a/i18n/gi18n/gi18n_manager.go b/i18n/gi18n/gi18n_manager.go index 20d90340564..dfe868fbcc4 100644 --- a/i18n/gi18n/gi18n_manager.go +++ b/i18n/gi18n/gi18n_manager.go @@ -9,6 +9,8 @@ package gi18n import ( "context" "fmt" + "io/fs" + "path/filepath" "strings" "sync" @@ -30,6 +32,7 @@ const ( pathTypeNone pathType = "none" pathTypeNormal pathType = "normal" pathTypeGres pathType = "gres" + pathTypeFS pathType = "stdfs" ) // Manager for i18n contents, it is concurrent safe, supporting hot reload. @@ -43,6 +46,7 @@ type Manager struct { // Options is used for i18n object configuration. type Options struct { + PathFs fs.FS // I18n files storage fs.FS. Path string // I18n files storage path. Language string // Default local language. Delimiters []string // Delimiters for variable parsing. @@ -68,7 +72,11 @@ func New(options ...Options) *Manager { var pathType = pathTypeNone if len(options) > 0 { opts = options[0] - pathType = opts.checkPathType(opts.Path) + if opts.PathFs != nil { + pathType = pathTypeFS + } else { + pathType = opts.checkPathType(opts.Path) + } } else { opts = Options{} for _, folder := range searchFolders { @@ -142,6 +150,13 @@ func (m *Manager) SetPath(path string) error { return nil } +func (m *Manager) SetPathFS(pathfs fs.FS) error { + m.pathType = pathTypeFS + m.options.PathFs = pathfs + m.reset() + return nil +} + // SetLanguage sets the language for translator. func (m *Manager) SetLanguage(language string) { m.options.Language = language @@ -245,6 +260,51 @@ func (m *Manager) init(ctx context.Context) { m.mu.Lock() defer m.mu.Unlock() switch m.pathType { + case pathTypeFS: + files1, err1 := fs.Glob(m.options.PathFs, "*.*") + files2, err2 := fs.Glob(m.options.PathFs, "*/*.*") + if err1 != nil || err2 != nil { + if err1 != nil { + intlog.Errorf(ctx, "load i18n files failed: %+v", err1) + } else { + intlog.Errorf(ctx, "load i18n files failed: %+v", err2) + } + return + } + files := append(files1, files2...) + if len(files) > 0 { + var ( + name string + lang string + array []string + content []byte + err error + ) + m.data = make(map[string]map[string]string) + for _, file := range files { + if content, err = fs.ReadFile(m.options.PathFs, file); err != nil { + intlog.Errorf(ctx, "get i18n file content failed: %+v", err) + continue + } + name = strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + array = strings.Split(file, "/") + if len(array) > 1 { + lang = array[0] + } else if len(array) == 1 { + lang = gfile.Name(array[0]) + } + if m.data[lang] == nil { + m.data[lang] = make(map[string]string) + } + if j, err := gjson.LoadContent(content); err == nil { + for k, v := range j.Var().Map() { + m.data[lang][k] = gconv.String(v) + } + } else { + intlog.Errorf(ctx, "load i18n file '%s' failed: %+v", name, err) + } + } + } case pathTypeGres: files := m.options.Resource.ScanDirFile(m.options.Path, "*.*", true) if len(files) > 0 { diff --git a/i18n/gi18n/gi18n_z_unit_test.go b/i18n/gi18n/gi18n_z_unit_test.go index af44d226bf7..7599e0ccbdf 100644 --- a/i18n/gi18n/gi18n_z_unit_test.go +++ b/i18n/gi18n/gi18n_z_unit_test.go @@ -7,6 +7,7 @@ package gi18n_test import ( + "os" "time" "github.com/gogf/gf/v2/encoding/gbase64" @@ -77,6 +78,58 @@ func Test_Basic(t *testing.T) { }) } +func Test_BasicFS(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + i18n := gi18n.New(gi18n.Options{ + PathFs: os.DirFS(gtest.DataPath("i18n")), + }) + i18n.SetLanguage("none") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + + i18n.SetLanguage("ja") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + + i18n.SetLanguage("zh-CN") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + i18n.SetDelimiters("{$", "}") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{$hello}{$world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{$你好} {$世界}"), "hello world") + // undefined variables. + t.Assert(i18n.T(context.Background(), "{$你好1}{$世界1}"), "{$你好1}{$世界1}") + }) + + gtest.C(t, func(t *gtest.T) { + i18n := gi18n.New(gi18n.Options{ + PathFs: os.DirFS(gtest.DataPath("i18n-file")), + }) + i18n.SetLanguage("none") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + + i18n.SetLanguage("ja") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + + i18n.SetLanguage("zh-CN") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#你好} {#世界}"), "hello world") + }) + + gtest.C(t, func(t *gtest.T) { + i18n := gi18n.New(gi18n.Options{ + PathFs: os.DirFS(gdebug.CallerDirectory() + gfile.Separator + "testdata" + gfile.Separator + "i18n-dir"), + }) + i18n.SetLanguage("none") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + + i18n.SetLanguage("ja") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + + i18n.SetLanguage("zh-CN") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + }) +} + func Test_TranslateFormat(t *testing.T) { // Tf gtest.C(t, func(t *gtest.T) { @@ -121,6 +174,36 @@ func Test_DefaultManager(t *testing.T) { }) } +func Test_FSDefaultManager(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + err := gi18n.SetPathFS(os.DirFS(gtest.DataPath("i18n"))) + t.AssertNil(err) + + gi18n.SetLanguage("none") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + + gi18n.SetLanguage("ja") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") + + gi18n.SetLanguage("zh-CN") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "你好世界") + }) + + gtest.C(t, func(t *gtest.T) { + err := gi18n.SetPathFS(os.DirFS(gdebug.CallerDirectory() + gfile.Separator + "testdata" + gfile.Separator + "i18n-dir")) + t.AssertNil(err) + + gi18n.SetLanguage("none") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + + gi18n.SetLanguage("ja") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "こんにちは世界") + + gi18n.SetLanguage("zh-CN") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "你好世界") + }) +} + func Test_Instance(t *testing.T) { gtest.C(t, func(t *gtest.T) { m := gi18n.Instance()