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
}