[add] Generating language packs during SUI compilation
This commit is contained in:
parent
e170f6017e
commit
6ff2ad2ead
7 changed files with 333 additions and 30 deletions
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:"-"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue