From ef8b8027cfaa020fcc80d210e77312c4f1f8151b Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 15 Jul 2024 09:24:44 +0800 Subject: [PATCH] [add] sui.trans.all and sui.trans.page to support automatic translation --- sui/api/process.go | 86 ++++++++++++++++ sui/api/process_test.go | 43 +++++++- sui/core/interfaces.go | 4 + sui/core/types.go | 6 +- sui/storages/local/build.go | 148 +++++++++++++++++++++++++--- sui/storages/local/build_test.go | 71 +++++++++++++ sui/storages/local/local_test.go | 4 +- sui/storages/local/template.go | 20 +++- sui/storages/local/template_test.go | 11 ++- 9 files changed, 370 insertions(+), 23 deletions(-) diff --git a/sui/api/process.go b/sui/api/process.go index ab572a3c..5459bb62 100644 --- a/sui/api/process.go +++ b/sui/api/process.go @@ -59,6 +59,9 @@ func init() { "build.all": BuildAll, "build.page": BuildPage, + "trans.all": TransAll, + "trans.page": TransPage, + "sync.assetfile": SyncAssetFile, // Will be deprecated or change in the future // Will be deprecated or change in the future @@ -1018,6 +1021,89 @@ func BuildPage(process *process.Process) interface{} { return nil } +// TransAll handle the render page request +func TransAll(process *process.Process) interface{} { + + process.ValidateArgNums(3) + sui := get(process) + templateID := process.ArgsString(1) + + option := process.ArgsMap(2, map[string]interface{}{}) + ssr := true + if v, ok := option["ssr"].(bool); ok { + ssr = v + } + + assetRoot := "" + if v, ok := option["asset_root"].(string); ok { + assetRoot = v + } + + data := map[string]interface{}{} + if v, ok := option["data"].(map[string]interface{}); ok { + data = v + } + + tmpl, err := sui.GetTemplate(templateID) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + warnings, err := tmpl.Trans(&core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data}) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + if warnings != nil && len(warnings) > 0 { + return warnings + } + return nil +} + +// TransPage handle the render page request +func TransPage(process *process.Process) interface{} { + process.ValidateArgNums(4) + sui := get(process) + templateID := process.ArgsString(1) + route := route(process, 2) + option := process.ArgsMap(3, map[string]interface{}{}) + ssr := true + if v, ok := option["ssr"].(bool); ok { + ssr = v + } + + assetRoot := "" + if v, ok := option["asset_root"].(string); ok { + assetRoot = v + } + + tmpl, err := sui.GetTemplate(templateID) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + page, err := tmpl.Page(route) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + err = page.Load() + if err != nil { + exception.New(err.Error(), 500).Throw() + } + + data := process.ArgsMap(5, map[string]interface{}{}) + warnings, err := page.Trans(nil, &core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data}) + if err != nil { + exception.New(err.Error(), 500).Throw() + } + if warnings != nil && len(warnings) > 0 { + return warnings + } + + return nil +} + // get the sui func get(process *process.Process) core.SUI { sui, has := core.SUIs[process.ArgsString(0)] diff --git a/sui/api/process_test.go b/sui/api/process_test.go index 3e5f3d34..7c6c33b0 100644 --- a/sui/api/process_test.go +++ b/sui/api/process_test.go @@ -87,10 +87,13 @@ func TestTemplateLocaleGet(t *testing.T) { } assert.IsType(t, []core.SelectOption{}, res) - assert.Equal(t, 3, len(res.([]core.SelectOption))) - assert.Equal(t, "ja-jp", res.([]core.SelectOption)[0].Value) + assert.Equal(t, 4, len(res.([]core.SelectOption))) + assert.Equal(t, "en-us", res.([]core.SelectOption)[0].Value) + assert.True(t, res.([]core.SelectOption)[0].Default) + assert.Equal(t, "zh-cn", res.([]core.SelectOption)[1].Value) assert.Equal(t, "zh-hk", res.([]core.SelectOption)[2].Value) + assert.Equal(t, "ja-jp", res.([]core.SelectOption)[3].Value) } func TestTemplateThemeGet(t *testing.T) { @@ -723,6 +726,42 @@ func TestBuildPage(t *testing.T) { assert.Nil(t, res) } +func TestTransAll(t *testing.T) { + prepare(t) + defer clean() + + // test demo + p, err := process.Of("sui.trans.all", "test", "advanced", map[string]interface{}{"ssr": true}) + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + + assert.Nil(t, res) +} + +func TestTransPage(t *testing.T) { + prepare(t) + defer clean() + + // test demo + p, err := process.Of("sui.trans.page", "test", "advanced", "/i18n", map[string]interface{}{"ssr": true}) + if err != nil { + t.Fatal(err) + } + + res, err := p.Exec() + if err != nil { + t.Fatal(err) + } + + assert.Nil(t, res) +} + func TestSyncAssetFile(t *testing.T) { prepare(t) defer clean() diff --git a/sui/core/interfaces.go b/sui/core/interfaces.go index 2f54d8d4..7149cba7 100644 --- a/sui/core/interfaces.go +++ b/sui/core/interfaces.go @@ -65,6 +65,8 @@ type ITemplate interface { ExecBeforeBuildScripts() []TemplateScirptResult ExecAfterBuildScripts() []TemplateScirptResult + + Trans(option *BuildOption) ([]string, error) } // IPage is the interface for the page @@ -94,6 +96,8 @@ type IPage interface { Build(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error) BuildAsComponent(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error) + + Trans(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error) } // IBlock is the interface for the block diff --git a/sui/core/types.go b/sui/core/types.go index c40c3998..51b73d80 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -186,6 +186,7 @@ type Template struct { Document []byte `json:"-"` GlobalData []byte `json:"-"` Scripts *TemplateScirpts `json:"scripts,omitempty"` + Translator string `json:"translator,omitempty"` } // TemplateScirpts is the struct for the template scripts @@ -217,8 +218,9 @@ type Theme struct { // SelectOption is the struct for the select option type SelectOption struct { - Label string `json:"label"` - Value string `json:"value"` + Label string `json:"label"` + Value string `json:"value"` + Default bool `json:"default"` } // Asset is the struct for the asset diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index 2b41d012..63859c20 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/go-multierror" "github.com/yaoapp/gou/application" + "github.com/yaoapp/gou/process" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" "gopkg.in/yaml.v3" @@ -127,6 +128,37 @@ func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) { return warnings, err } +// Trans the template +func (tmpl *Template) Trans(option *core.BuildOption) ([]string, error) { + var err error + warnings := []string{} + + ctx := core.NewGlobalBuildContext() + pages, err := tmpl.Pages() + if err != nil { + return warnings, err + } + + // loaed pages + for _, page := range pages { + err := page.Load() + if err != nil { + return warnings, err + } + + messages, err := page.Trans(ctx, option) + if err != nil { + return warnings, err + } + + if len(messages) > 0 { + warnings = append(warnings, messages...) + } + } + + return warnings, nil +} + // SyncAssetFile sync the assets func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error { @@ -309,6 +341,25 @@ func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *c return warnings, err } +// Trans the page +func (page *Page) Trans(globalCtx *core.GlobalBuildContext, option *core.BuildOption) ([]string, error) { + warnings := []string{} + ctx := core.NewBuildContext(globalCtx) + + _, messages, err := page.Page.CompileAsComponent(ctx, option) + if err != nil { + return warnings, err + } + + if len(messages) > 0 { + warnings = append(warnings, messages...) + } + + // Tranlate the locale files + err = page.writeLocaleSource(ctx) + return warnings, err +} + func (page *Page) publicFile(data map[string]interface{}) string { root, err := page.tmpl.local.DSL.PublicRoot(data) if err != nil { @@ -328,6 +379,9 @@ func (page *Page) localeFiles(data map[string]interface{}) map[string]string { roots := map[string]string{} locales := page.tmpl.Locales() for _, locale := range locales { + if locale.Default { + continue + } target := filepath.Join("/", "public", root, ".locales", locale.Value, fmt.Sprintf("%s.yml", page.Route)) roots[locale.Value] = target } @@ -365,14 +419,13 @@ func (page *Page) localeGlobal(name string) core.Locale { return global } -func (page *Page) locale(name string) core.Locale { +func (page *Page) locale(name string, pageOnly ...bool) core.Locale { file := filepath.Join(page.tmpl.Root, "__locales", name, fmt.Sprintf("%s.yml", page.Route)) global := page.localeGlobal(name) // Check the locale file exist, err := page.tmpl.local.fs.Exists(file) if err != nil { - log.Error(`[SUI] Check the locale file error: %s`, err.Error()) return global } @@ -399,22 +452,95 @@ func (page *Page) locale(name string) core.Locale { return global } - // Merge the global - for key, message := range global.Keys { - if _, ok := locale.Keys[key]; !ok { - locale.Keys[key] = message - } - } + if len(pageOnly) == 0 || !pageOnly[0] { - for key, message := range global.Messages { - if _, ok := locale.Messages[key]; !ok { - locale.Messages[key] = message + // Merge the global + for key, message := range global.Keys { + if _, ok := locale.Keys[key]; !ok { + locale.Keys[key] = message + } + } + + for key, message := range global.Messages { + if _, ok := locale.Messages[key]; !ok { + locale.Messages[key] = message + } } } return locale } +func (page *Page) writeLocaleSource(ctx *core.BuildContext) error { + + locales := page.tmpl.Locales() + translations := ctx.GetTranslations() + for _, lc := range locales { + if lc.Default { + continue + } + + locale := page.locale(lc.Value, true) + for _, t := range translations { + message := t.Message + // Match the key + if _, has := locale.Messages[message]; has { + message = locale.Messages[message] + } + locale.Keys[t.Key] = message + msg, has := locale.Messages[t.Message] + if has && msg != t.Message { + continue + } + locale.Messages[t.Message] = t.Message + } + + // Call the hook + var keys any = locale.Keys + var messages any = locale.Messages + if page.tmpl.Translator != "" { + p, err := process.Of(page.tmpl.Translator, lc.Value, locale, page.Route, page.TemplateID) + if err != nil { + return err + } + + res, err := p.Exec() + if err != nil { + return err + } + + pres, ok := res.(map[string]interface{}) + if !ok { + return fmt.Errorf("The translator %s should return a locale", page.tmpl.Translator) + } + + keys = pres["keys"] + messages = pres["messages"] + } + + if keys == nil && messages == nil { + return nil + } + + // Save to file + file := filepath.Join(page.tmpl.Root, "__locales", lc.Value, fmt.Sprintf("%s.yml", page.Route)) + content, err := yaml.Marshal(map[string]interface{}{ + "keys": keys, + "messages": messages, + }) + if err != nil { + return err + } + + _, err = page.tmpl.local.fs.WriteFile(file, content, 0644) + if err != nil { + return err + } + } + + return nil +} + func (page *Page) writeLocaleFiles(ctx *core.BuildContext, data map[string]interface{}) error { if ctx == nil { diff --git a/sui/storages/local/build_test.go b/sui/storages/local/build_test.go index 31f7a0ac..d42549bb 100644 --- a/sui/storages/local/build_test.go +++ b/sui/storages/local/build_test.go @@ -198,3 +198,74 @@ func TestPageBuildAsComponent(t *testing.T) { assert.Contains(t, string(content), "function Init()") assert.Contains(t, string(content), `type="flowbite-edit-select"`) } + +func TestPageTrans(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Test.GetTemplate("advanced") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + root := application.App.Root() + path := filepath.Join(root, "data", tmpl.GetRoot(), "__locales") + + // Remove files and directories in Public directory if exists + err = os.RemoveAll(path) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("RemoveAll error: %v", err) + } + + page, err := tmpl.Page("/i18n") + if err != nil { + t.Fatalf("Page error: %v", err) + } + + warnings, err := page.Trans(nil, &core.BuildOption{SSR: true, AssetRoot: "/unit-test/assets"}) + if err != nil { + t.Fatalf("Page Build error: %v", err) + } + + assert.DirExists(t, path) + assert.DirExists(t, filepath.Join(path, "zh-cn")) + assert.DirExists(t, filepath.Join(path, "zh-hk")) + assert.DirExists(t, filepath.Join(path, "ja-jp")) + assert.FileExists(t, filepath.Join(path, "zh-cn", "i18n.yml")) + assert.FileExists(t, filepath.Join(path, "zh-hk", "i18n.yml")) + assert.FileExists(t, filepath.Join(path, "ja-jp", "i18n.yml")) + assert.Len(t, warnings, 0) +} + +func TestTemplateTrans(t *testing.T) { + tests := prepare(t) + defer clean() + + tmpl, err := tests.Test.GetTemplate("advanced") + if err != nil { + t.Fatalf("GetTemplate error: %v", err) + } + + root := application.App.Root() + path := filepath.Join(root, "data", tmpl.GetRoot(), "__locales") + + // Remove files and directories in Public directory if exists + err = os.RemoveAll(path) + if err != nil && !os.IsNotExist(err) { + t.Fatalf("RemoveAll error: %v", err) + } + + warnings, err := tmpl.Trans(&core.BuildOption{SSR: true}) + if err != nil { + t.Fatalf("Components error: %v", err) + } + + assert.DirExists(t, path) + assert.DirExists(t, filepath.Join(path, "zh-cn")) + assert.DirExists(t, filepath.Join(path, "zh-hk")) + assert.DirExists(t, filepath.Join(path, "ja-jp")) + assert.FileExists(t, filepath.Join(path, "zh-cn", "i18n.yml")) + assert.FileExists(t, filepath.Join(path, "zh-hk", "i18n.yml")) + assert.FileExists(t, filepath.Join(path, "ja-jp", "i18n.yml")) + assert.Len(t, warnings, 0) +} diff --git a/sui/storages/local/local_test.go b/sui/storages/local/local_test.go index 73646d1c..b44f1ac4 100644 --- a/sui/storages/local/local_test.go +++ b/sui/storages/local/local_test.go @@ -31,7 +31,7 @@ func TestGetTemplates(t *testing.T) { assert.Equal(t, "advanced", testTmpls[0].(*Template).ID) assert.Equal(t, "The advanced template", testTmpls[0].(*Template).Name) assert.Len(t, testTmpls[0].Themes(), 2) - assert.Len(t, testTmpls[0].Locales(), 3) + assert.Len(t, testTmpls[0].Locales(), 4) assert.Len(t, testTmpls[0].(*Template).Template.Themes, 2) assert.Len(t, testTmpls[0].(*Template).Template.Locales, 4) @@ -57,7 +57,7 @@ func TestGetTemplates(t *testing.T) { assert.Equal(t, "default", webTmpls[0].(*Template).ID) assert.Equal(t, "Yao Startup Webapp", webTmpls[0].(*Template).Name) assert.Len(t, webTmpls[0].Themes(), 2) - assert.Len(t, webTmpls[0].Locales(), 3) + assert.Len(t, webTmpls[0].Locales(), 4) assert.Len(t, webTmpls[0].(*Template).Template.Themes, 2) assert.Len(t, webTmpls[0].(*Template).Template.Locales, 2) } diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index 0eaa5de8..6928b6f7 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -168,20 +168,36 @@ func (tmpl *Template) Locales() []core.SelectOption { return tmpl.locales } + // Defined the support locales supportLocales := []core.SelectOption{} + localeMap := map[string]bool{} + locales := tmpl.Template.Locales + for _, locale := range locales { + if localeMap[locale.Value] { + continue + } + localeMap[locale.Value] = true + supportLocales = append(supportLocales, locale) + } + path := filepath.Join(tmpl.Root, "__locales") if !tmpl.local.fs.IsDir(path) { - return nil + return supportLocales } dirs, err := tmpl.local.fs.ReadDir(path, false) if err != nil { - return nil + return supportLocales } + // Get the support locales for _, dir := range dirs { locale := filepath.Base(dir) + if localeMap[locale] { + continue + } label := language.Make(locale).String() + localeMap[locale] = true supportLocales = append(supportLocales, core.SelectOption{ Value: locale, Label: label, diff --git a/sui/storages/local/template_test.go b/sui/storages/local/template_test.go index 0e429605..d098054a 100644 --- a/sui/storages/local/template_test.go +++ b/sui/storages/local/template_test.go @@ -41,14 +41,17 @@ func TestTemplateLocales(t *testing.T) { t.Fatalf("Locales error: %v", len(locales)) } - assert.Equal(t, "ja-JP", locales[0].Label) - assert.Equal(t, "ja-jp", locales[0].Value) + assert.Equal(t, "English", locales[0].Label) + assert.Equal(t, "en-us", locales[0].Value) - assert.Equal(t, "zh-CN", locales[1].Label) + assert.Equal(t, "简体中文", locales[1].Label) assert.Equal(t, "zh-cn", locales[1].Value) - assert.Equal(t, "zh-HK", locales[2].Label) + assert.Equal(t, "繁體中文", locales[2].Label) assert.Equal(t, "zh-hk", locales[2].Value) + + assert.Equal(t, "日本語", locales[3].Label) + assert.Equal(t, "ja-jp", locales[3].Value) } func TestTemplateAsset(t *testing.T) {