[add] Generating language packs during SUI compilation

This commit is contained in:
Max 2024-06-27 17:15:36 +08:00
parent e170f6017e
commit 6ff2ad2ead
7 changed files with 333 additions and 30 deletions

View file

@ -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("<!-- %s -->", 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("<script type=\"text/javascript\">\nfunction %s(){\n%s\n}\n</script>\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("<script type=\"text/javascript\">\n%s\n</script>\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("<script type=\"text/javascript\">\nfunction %s(){\n%s\n}\n</script>\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
}

View file

@ -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,

View file

@ -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:"-"`
}

View file

@ -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))

View file

@ -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)},

View file

@ -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

View file

@ -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
}