From 6ff2ad2ead2bff930bc2d8f42f7fef9008d7a5b0 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 27 Jun 2024 17:15:36 +0800 Subject: [PATCH] [add] Generating language packs during SUI compilation --- sui/core/build.go | 99 +++++++++++++++++++- sui/core/compile.go | 4 +- sui/core/types.go | 68 +++++++++++--- sui/storages/local/build.go | 159 ++++++++++++++++++++++++++++++++- sui/storages/local/page.go | 22 ++--- sui/storages/local/template.go | 6 +- sui/storages/local/types.go | 5 +- 7 files changed, 333 insertions(+), 30 deletions(-) diff --git a/sui/core/build.go b/sui/core/build.go index 3e6cf189..76501607 100644 --- a/sui/core/build.go +++ b/sui/core/build.go @@ -10,10 +10,13 @@ import ( "github.com/PuerkitoBio/goquery" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/kun/log" + "golang.org/x/net/html" ) var slotRe = regexp.MustCompile(`\[\{([^\}]+)\}\]`) var cssRe = regexp.MustCompile(`([\.a-z0-9A-Z-:# ]+)\{`) +var langFuncRe = regexp.MustCompile(`L\s*\(\s*["'](.*?)["']\s*\)`) +var langAttrRe = regexp.MustCompile(`'::(.*?)'`) // Build is the struct for the public func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error) { @@ -44,7 +47,7 @@ func (page *Page) Build(option *BuildOption) (*goquery.Document, []string, error doc.Selection.Find("head").AppendHtml(style) // Add Script - code, scripts, err := page.BuildScript(option) + code, scripts, err := page.BuildScript(option, option.Namespace) if err != nil { warnings = append(warnings, err.Error()) } @@ -100,7 +103,7 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface warnings = append(warnings, err.Error()) } - code, _, err := page.BuildScript(option) + code, _, err := page.BuildScript(option, option.Namespace) if err != nil { warnings = append(warnings, err.Error()) } @@ -125,6 +128,12 @@ func (page *Page) BuildForImport(option *BuildOption, slots map[string]interface func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []string) error { pages := doc.Find("*").FilterFunction(func(i int, sel *goquery.Selection) bool { + + // Get the translation + if translations := getNodeTranslation(sel, i, option.Namespace); len(translations) > 0 { + page.Translations = append(page.Translations, translations...) + } + tagName := sel.Get(0).Data if tagName == "page" { return true @@ -218,6 +227,9 @@ func (page *Page) parse(doc *goquery.Document, option *BuildOption, warnings []s Namespace: namespace, }, slots, attrs) + // append translations + page.Translations = append(page.Translations, p.Translations...) + if err != nil { sel.ReplaceWith(fmt.Sprintf("", err.Error())) log.Warn("Page %s/%s/%s: %s", page.SuiID, page.TemplateID, page.Route, err.Error()) @@ -313,7 +325,7 @@ func (page *Page) BuildStyle(option *BuildOption) (string, error) { } // BuildScript build the script -func (page *Page) BuildScript(option *BuildOption) (string, []string, error) { +func (page *Page) BuildScript(option *BuildOption, namespace string) (string, []string, error) { if page.Codes.JS.Code == "" && page.Codes.TS.Code == "" { return "", nil, nil @@ -345,7 +357,7 @@ func (page *Page) BuildScript(option *BuildOption) (string, []string, error) { return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil } - code, scripts, err := page.CompileJS([]byte(page.Codes.JS.Code), false) + code, scripts, err := page.CompileJS([]byte(page.Codes.JS.Code), true) if err != nil { return "", nil, err } @@ -367,6 +379,11 @@ func (page *Page) BuildScript(option *BuildOption) (string, []string, error) { return fmt.Sprintf("\n", code), scripts, nil } + // Get the translation + if translations := getScriptTranslation(string(code), namespace); len(translations) > 0 { + page.Translations = append(page.Translations, translations...) + } + return fmt.Sprintf("\n", option.Namespace, addTabToEachLine(string(code))), scripts, nil } @@ -387,3 +404,77 @@ func addTabToEachLine(input string, prefix ...string) string { return strings.Join(lines, "\n") } + +func getScriptTranslation(code string, namespace string) []Translation { + translations := []Translation{} + matches := langFuncRe.FindAllStringSubmatch(code, -1) + for i, match := range matches { + translations = append(translations, Translation{ + Key: fmt.Sprintf("%s_script_%d", namespace, i), + Message: match[1], + Type: "script", + }) + } + return translations +} + +func getNodeTranslation(sel *goquery.Selection, index int, namespace string) []Translation { + + translations := []Translation{} + nodeType := sel.Get(0).Type + switch nodeType { + case html.ElementNode: + + // Get the translation + if typ, has := sel.Attr("s:trans"); has { + typ = strings.TrimSpace(typ) + if typ == "" { + typ = "html" + } + translations = append(translations, Translation{ + Key: fmt.Sprintf("%s_index_%d", namespace, index), + Message: strings.TrimSpace(sel.Text()), + Type: typ, + }) + } + + // Attributes + for i, attr := range sel.Get(0).Attr { + + // value="::attr" + if strings.HasPrefix(attr.Val, "::") { + translations = append(translations, Translation{ + Key: fmt.Sprintf("%s_index_attr_%d_%d", namespace, index, i), + Message: attr.Val[2:], + Name: attr.Key, + Type: "attr", + }) + } + + // value="{{ 'key': '::value' }}" + matches := langAttrRe.FindAllStringSubmatch(attr.Val, -1) + if len(matches) > 0 { + for j, match := range matches { + translations = append(translations, Translation{ + Key: fmt.Sprintf("%s_index_attr_%d_%d_%d", namespace, index, i, j), + Message: match[1], + Name: attr.Key, + Type: "attr", + }) + } + } + } + + case html.TextNode: + if strings.HasPrefix(sel.Text(), "::") { + translations = append(translations, Translation{ + Key: fmt.Sprintf("%s_index_%d", namespace, index), + Message: strings.TrimSpace(sel.Text()[2:]), + Type: "text", + }) + } + } + + return translations + +} diff --git a/sui/core/compile.go b/sui/core/compile.go index 35da3ad3..61b08e72 100644 --- a/sui/core/compile.go +++ b/sui/core/compile.go @@ -78,7 +78,7 @@ func (page *Page) CompileJS(source []byte, minify bool) ([]byte, []string, error } jsCode := importRe.ReplaceAllString(string(source), "") if minify { - minified, err := transform.MinifyJS(jsCode) + minified, err := transform.MinifyJS(jsCode, api.ES2015) return []byte(minified), scripts, err } return []byte(jsCode), scripts, nil @@ -99,7 +99,7 @@ func (page *Page) CompileTS(source []byte, minify bool) ([]byte, []string, error tsCode := importRe.ReplaceAllString(string(source), "") if minify { jsCode, err := transform.TypeScript(string(tsCode), api.TransformOptions{ - Target: api.ESNext, + Target: api.ES2015, MinifyWhitespace: true, MinifyIdentifiers: true, MinifySyntax: true, diff --git a/sui/core/types.go b/sui/core/types.go index e8537b5b..1a43a3d8 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -25,16 +25,63 @@ type Setting struct { // Page is the struct for the page type Page struct { - Route string `json:"route"` - Name string `json:"name,omitempty"` - TemplateID string `json:"-"` - SuiID string `json:"-"` - Config *PageConfig `json:"-"` - Path string `json:"-"` - Codes SourceCodes `json:"-"` - Document []byte `json:"-"` - GlobalData []byte `json:"-"` - Attrs map[string]string `json:"-"` + Route string `json:"route"` + Name string `json:"name,omitempty"` + TemplateID string `json:"-"` + SuiID string `json:"-"` + Config *PageConfig `json:"-"` + Path string `json:"-"` + Codes SourceCodes `json:"-"` + Document []byte `json:"-"` + GlobalData []byte `json:"-"` + Attrs map[string]string `json:"-"` + Translations []Translation `json:"-"` +} + +// Translation is the struct for the translation +type Translation struct { + Key string `json:"key,omitempty"` + Name string `json:"name,omitempty"` + Message string `json:"message,omitempty"` + Type string `json:"type,omitempty"` // ENUM: 'text', 'html', 'attr', 'script' +} + +// Locale is the struct for the locale +type Locale struct { + Keys map[string]string `json:"keys,omitempty"` + Messages map[string]string `json:"messages,omitempty"` + Date LocaleDate `json:"date,omitempty"` + Currency LocaleCurrency `json:"currency,omitempty"` + Number LocaleNumber `json:"number,omitempty"` +} + +// LocaleDate the struct for the locale date format +type LocaleDate struct { + Short string `json:"short,omitempty"` + Long string `json:"long,omitempty"` + Full string `json:"full,omitempty"` + Month string `json:"month,omitempty"` + Week string `json:"week,omitempty"` + Year string `json:"year,omitempty"` + Day string `json:"day,omitempty"` + Human string `json:"human,omitempty"` +} + +// LocaleCurrency the struct for the locale currency +type LocaleCurrency struct { + Format string `json:"format,omitempty"` + Unit string `json:"unit,omitempty"` + Separator string `json:"separator,omitempty"` + Delimiter string `json:"delimiter,omitempty"` + Precision int `json:"precision,omitempty"` +} + +// LocaleNumber the struct for the locale number +type LocaleNumber struct { + Format string `json:"format,omitempty"` + Separator string `json:"separator,omitempty"` + Delimiter string `json:"delimiter,omitempty"` + Precision int `json:"precision,omitempty"` } // PageTreeNode is the struct for the page tree node @@ -87,6 +134,7 @@ type Template struct { Descrption string `json:"description"` Screenshots []string `json:"screenshots"` Themes []SelectOption `json:"themes"` + Locales []SelectOption `json:"locales"` Document []byte `json:"-"` GlobalData []byte `json:"-"` } diff --git a/sui/storages/local/build.go b/sui/storages/local/build.go index 71991db8..9b910f5b 100644 --- a/sui/storages/local/build.go +++ b/sui/storages/local/build.go @@ -9,6 +9,7 @@ import ( "github.com/yaoapp/gou/application" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" + "gopkg.in/yaml.v3" ) // Build the template @@ -124,7 +125,13 @@ func (page *Page) Build(option *core.BuildOption) error { } // Save the html - return page.writeHTML([]byte(html), option.Data) + err = page.writeHTML([]byte(html), option.Data) + if err != nil { + return err + } + + // Save the locale files + return page.writeLocaleFiles(option.Data) } func (page *Page) publicFile(data map[string]interface{}) string { @@ -136,6 +143,156 @@ func (page *Page) publicFile(data map[string]interface{}) string { return filepath.Join("/", "public", root, page.Route) } +func (page *Page) localeFiles(data map[string]interface{}) map[string]string { + root, err := page.tmpl.local.DSL.PublicRoot(data) + if err != nil { + log.Error("publicFile: Get the public root error: %s. use %s", err.Error(), page.tmpl.local.DSL.Public.Root) + root = page.tmpl.local.DSL.Public.Root + } + + roots := map[string]string{} + locales := page.tmpl.Locales() + for _, locale := range locales { + target := filepath.Join("/", "public", root, ".locales", locale.Value, fmt.Sprintf("%s.yml", page.Route)) + roots[locale.Value] = target + } + return roots +} + +func (page *Page) localeGlobal(name string) core.Locale { + global := core.Locale{ + Keys: map[string]string{}, + Messages: map[string]string{}, + } + file := filepath.Join(page.tmpl.Root, "__locales", name, "__global.yml") + exist, err := page.tmpl.local.fs.Exists(file) + if err != nil { + log.Error(`[SUI] Check the global locale file error: %s`, err.Error()) + return global + } + + if !exist { + return global + } + + raw, err := page.tmpl.local.fs.ReadFile(file) + if err != nil { + log.Error(`[SUI] Read the global locale file error: %s`, err.Error()) + return global + } + + err = yaml.Unmarshal(raw, &global) + if err != nil { + log.Error(`[SUI] Parse the global locale file error: %s`, err.Error()) + return global + } + + return global +} + +func (page *Page) locale(name string) 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 + } + + if !exist { + return global + } + + locale := core.Locale{ + Keys: map[string]string{}, + Messages: map[string]string{}, + Date: global.Date, + Currency: global.Currency, + Number: global.Number, + } + raw, err := page.tmpl.local.fs.ReadFile(file) + if err != nil { + log.Error(`[SUI] Read the locale file error: %s`, err.Error()) + return global + } + + err = yaml.Unmarshal(raw, &locale) + if err != nil { + log.Error(`[SUI] Parse the locale file error: %s`, err.Error()) + return global + } + + // 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) writeLocaleFiles(data map[string]interface{}) error { + + // No translations + if len(page.Page.Translations) == 0 { + return nil + } + + keys := map[string]string{} + messages := map[string]string{} + for _, t := range page.Page.Translations { + keys[t.Key] = t.Message + messages[t.Message] = t.Message + } + + files := page.localeFiles(data) + for name, file := range files { + locale := page.locale(name) + for key := range keys { + if _, has := locale.Keys[key]; has { + keys[key] = locale.Keys[key] + } + } + + for message := range messages { + if _, has := locale.Messages[message]; has { + messages[message] = locale.Messages[message] + } + } + + locale.Keys = keys + locale.Messages = messages + raw, err := yaml.Marshal(locale) + if err != nil { + log.Error(`[SUI] Marshal the locale file error: %s`, err.Error()) + return err + } + + fileAbs := filepath.Join(application.App.Root(), file) + dir := filepath.Dir(fileAbs) + if exist, _ := os.Stat(dir); exist == nil { + os.MkdirAll(dir, os.ModePerm) + } + + err = os.WriteFile(fileAbs, raw, 0644) + if err != nil { + log.Error(`[SUI] Write the locale file error: %s`, err.Error()) + return err + } + } + + return nil +} + // writeHTMLTo write the html to file func (page *Page) writeHTML(html []byte, data map[string]interface{}) error { htmlFile := fmt.Sprintf("%s.sui", page.publicFile(data)) diff --git a/sui/storages/local/page.go b/sui/storages/local/page.go index f16a423f..4d557bfb 100644 --- a/sui/storages/local/page.go +++ b/sui/storages/local/page.go @@ -279,11 +279,12 @@ func (tmpl *Template) CreateEmptyPage(route string, setting *core.PageSetting) ( page := &Page{ tmpl: tmpl, Page: &core.Page{ - Route: route, - TemplateID: tmpl.ID, - SuiID: tmpl.local.ID, - Path: filepath.Join(tmpl.Root, route), - Name: name, + Route: route, + TemplateID: tmpl.ID, + SuiID: tmpl.local.ID, + Path: filepath.Join(tmpl.Root, route), + Name: name, + Translations: []core.Translation{}, Codes: core.SourceCodes{ HTML: core.Source{File: fmt.Sprintf("%s.html", name)}, CSS: core.Source{File: fmt.Sprintf("%s.css", name)}, @@ -349,11 +350,12 @@ func (tmpl *Template) getPage(route, file string) (core.IPage, error) { return &Page{ tmpl: tmpl, Page: &core.Page{ - Route: route, - Path: path, - Name: name, - TemplateID: tmpl.ID, - SuiID: tmpl.local.ID, + Route: route, + Path: path, + Name: name, + TemplateID: tmpl.ID, + SuiID: tmpl.local.ID, + Translations: []core.Translation{}, Codes: core.SourceCodes{ HTML: core.Source{File: fmt.Sprintf("%s%s", name, filepath.Ext(file))}, CSS: core.Source{File: fmt.Sprintf("%s.css", name)}, diff --git a/sui/storages/local/template.go b/sui/storages/local/template.go index 088336b5..398cbfc4 100644 --- a/sui/storages/local/template.go +++ b/sui/storages/local/template.go @@ -26,6 +26,9 @@ func (tmpl *Template) GetRoot() string { // Locales get the global locales func (tmpl *Template) Locales() []core.SelectOption { + if tmpl.locales != nil { + return tmpl.locales + } supportLocales := []core.SelectOption{} path := filepath.Join(tmpl.Root, "__locales") @@ -47,7 +50,8 @@ func (tmpl *Template) Locales() []core.SelectOption { }) } - return supportLocales + tmpl.locales = supportLocales + return tmpl.locales } // Themes get the global themes diff --git a/sui/storages/local/types.go b/sui/storages/local/types.go index 794e2387..a4521860 100644 --- a/sui/storages/local/types.go +++ b/sui/storages/local/types.go @@ -14,8 +14,9 @@ type Local struct { // Template is the struct for the local sui template type Template struct { - Root string `json:"-"` - local *Local + Root string `json:"-"` + local *Local + locales []core.SelectOption *core.Template }