refactor i18n support

This commit is contained in:
Max 2024-07-11 20:20:36 +08:00
parent da955f2c48
commit 61205ced5f
7 changed files with 206 additions and 140 deletions

View file

@ -7,15 +7,11 @@ import (
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/fatih/color"
jsoniter "github.com/json-iterator/go"
"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 build the page
func (page *Page) Build(ctx *BuildContext, option *BuildOption) (*goquery.Document, []string, error) {
@ -500,104 +496,3 @@ 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"
}
key := fmt.Sprintf("%s_index_%d", namespace, index)
translations = append(translations, Translation{
Key: key,
Message: strings.TrimSpace(sel.Text()),
Type: typ,
})
sel.SetAttr("s:trans-node", key)
sel.RemoveAttr("s:trans")
}
// Attributes
keys := map[string][]string{}
has := false
for i, attr := range sel.Get(0).Attr {
keys[attr.Key] = []string{}
// value="::attr"
if strings.HasPrefix(attr.Val, "::") {
key := fmt.Sprintf("%s_index_attr_%d_%d", namespace, index, i)
translations = append(translations, Translation{
Key: fmt.Sprintf("%s_index_attr_%d_%d", namespace, index, i),
Message: attr.Val[2:],
Name: attr.Key,
Type: "attr",
})
keys[attr.Key] = append(keys[attr.Key], key)
has = true
}
// value="{{ 'key': '::value' }}"
matches := langAttrRe.FindAllStringSubmatch(attr.Val, -1)
if len(matches) > 0 {
for j, match := range matches {
key := fmt.Sprintf("%s_index_attr_%d_%d_%d", namespace, index, i, j)
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",
})
keys[attr.Key] = append(keys[attr.Key], key)
has = true
}
}
}
if has {
raw, err := jsoniter.Marshal(keys)
if err != nil {
fmt.Println(color.RedString(err.Error()))
break
}
sel.SetAttr("s:trans-attrs", string(raw))
sel.RemoveAttr("s:trans")
}
case html.TextNode:
if strings.HasPrefix(sel.Text(), "::") {
key := fmt.Sprintf("%s_index_%d", namespace, index)
translations = append(translations, Translation{
Key: fmt.Sprintf("%s_index_%d", namespace, index),
Message: strings.TrimSpace(sel.Text()[2:]),
Type: "text",
})
sel.SetAttr("s:trans-node", key)
sel.RemoveAttr("s:trans")
}
}
return translations
}

View file

@ -5,15 +5,20 @@ import (
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/evanw/esbuild/pkg/api"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/runtime/transform"
"github.com/yaoapp/kun/log"
"golang.org/x/net/html"
)
var quoteRe = "'\"`"
var importRe = regexp.MustCompile(`import\s*\t*\n*[^;]*;`) // import { foo, bar } from 'hello'; ...
var importAssetsRe = regexp.MustCompile(`import\s*\t*\n*\s*['"]@assets\/([^'"]+)['"];`) // import '@assets/foo.js'; or import "@assets/foo.js";
var transStmtReSingle = regexp.MustCompile(`'::([^:']+)'`)
var transStmtReDouble = regexp.MustCompile(`"::([^:"]+)"`)
var transFuncRe = regexp.MustCompile(`__m\s*\(\s*["'](.*?)["']\s*\)`)
// AssetsRe is the regexp for assets
var AssetsRe = regexp.MustCompile(`[` + quoteRe + `]@assets\/([^` + quoteRe + `]+)[` + quoteRe + `]`) // '@assets/foo.js' or "@assets/foo.js" or `@assets/foo`
@ -85,6 +90,13 @@ func (page *Page) Compile(ctx *BuildContext, option *BuildOption) (string, []str
)
}
// Add the translation marks
sequence := 0
err = page.TranslateMarks(ctx, option, doc, &sequence)
if err != nil {
return "", warnings, err
}
page.ReplaceDocument(doc)
html, err := doc.Html()
if err != nil {
@ -140,6 +152,13 @@ func (page *Page) CompileAsComponent(ctx *BuildContext, option *BuildOption) (st
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="styles" type="json">%s</script>`+"\n", rawStyles))
body.Children().First().AppendHtml(fmt.Sprintf(`<script name="option" type="json">%s</script>`+"\n", rawOption))
// Add the translation marks
sequence := 0
err = page.TranslateMarks(ctx, option, doc, &sequence)
if err != nil {
return "", warnings, err
}
html, err := body.Html()
return html, warnings, err
}
@ -263,3 +282,146 @@ func (style StyleNode) HTML() string {
return "<style " + strings.Join(attrs, " ") + ">\n" + style.Source + "\n</style>"
}
// TranslateMarks add the translation marks to the document
func (page *Page) TranslateMarks(ctx *BuildContext, option *BuildOption, doc *goquery.Document, sequence *int) error {
if doc.Length() == 0 {
return nil
}
if ctx.translations == nil {
ctx.translations = []Translation{}
}
root := doc.First()
translations, err := page.translateNode(root.Nodes[0], sequence)
if err != nil {
return err
}
if translations != nil {
ctx.translations = append(ctx.translations, translations...)
}
return nil
}
func (page *Page) translateNode(node *html.Node, sequence *int) ([]Translation, error) {
translations := []Translation{}
*sequence = *sequence + 1
switch node.Type {
case html.DocumentNode:
for child := node.FirstChild; child != nil; child = child.NextSibling {
trans, err := page.translateNode(child, sequence)
if err != nil {
return nil, err
}
translations = append(translations, trans...)
}
break
case html.ElementNode:
// Script
if node.Data == "script" {
code := goquery.NewDocumentFromNode(node).Text()
if code != "" {
translations := []Translation{}
matches := transFuncRe.FindAllStringSubmatch(code, -1)
for _, match := range matches {
key := Namespace(page.Route, *sequence)
translations = append(translations, Translation{
Key: key,
Message: match[1],
Type: "script",
})
*sequence = *sequence + 1
}
}
break
}
sel := goquery.NewDocumentFromNode(node)
for _, attr := range node.Attr {
trans, keys, err := page.translateText(attr.Val, sequence, "attr")
if err != nil {
return nil, err
}
if len(keys) > 0 {
raw := strings.Join(keys, ",")
sel.SetAttr("s:trans-attr-"+attr.Key, raw)
translations = append(translations, trans...)
}
}
// Node Attributes
for child := node.FirstChild; child != nil; child = child.NextSibling {
trans, err := page.translateNode(child, sequence)
if err != nil {
return nil, err
}
translations = append(translations, trans...)
}
break
case html.TextNode:
parentSel := goquery.NewDocumentFromNode(node.Parent)
if _, has := parentSel.Attr("s:trans"); has {
key := Namespace(page.Route, *sequence)
message := strings.TrimSpace(node.Data)
if message != "" {
translations = append(translations, Translation{
Key: key,
Message: message,
Type: "text",
})
parentSel.SetAttr("s:trans-node", key)
*sequence = *sequence + 1
}
parentSel.RemoveAttr("s:trans")
}
trans, keys, err := page.translateText(node.Data, sequence, "text")
if err != nil {
return nil, err
}
if len(keys) > 0 {
raw := strings.Join(keys, ",")
parentSel.SetAttr("s:trans-text", raw)
parentSel.RemoveAttr("s:trans")
translations = append(translations, trans...)
}
break
}
return translations, nil
}
func (page *Page) translateText(text string, sequence *int, typ string) ([]Translation, []string, error) {
translations := []Translation{}
matches := stmtRe.FindAllStringSubmatch(text, -1)
keys := []string{}
for _, match := range matches {
text := strings.TrimSpace(match[1])
transMatches := transStmtReSingle.FindAllStringSubmatch(text, -1)
if len(transMatches) == 0 {
transMatches = transStmtReDouble.FindAllStringSubmatch(text, -1)
}
for _, transMatch := range transMatches {
message := strings.TrimSpace(transMatch[1])
key := Namespace(page.Route, *sequence)
keys = append(keys, key)
translations = append(translations, Translation{
Key: key,
Message: message,
Type: typ,
})
*sequence = *sequence + 1
}
}
return translations, keys, nil
}

View file

@ -36,6 +36,14 @@ func (ctx *BuildContext) GetJitComponents() []string {
return jitComponents
}
// GetTranslations get the translations
func (ctx *BuildContext) GetTranslations() []Translation {
if ctx.translations == nil {
return []Translation{}
}
return ctx.translations
}
// GetJitComponents get the just in time components
func (globalCtx *GlobalBuildContext) GetJitComponents() []string {
if globalCtx.jitComponents == nil {

View file

@ -70,8 +70,8 @@ const initScriptTmpl = `
`
const i118nScriptTmpl = `
function L(key) {
return key;
function __m(message) {
return message;
}
`

View file

@ -29,21 +29,20 @@ type Setting struct {
// Page is the struct for the page
type Page struct {
Route string `json:"route"`
Name string `json:"name,omitempty"`
CacheStore string `json:"-"`
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:"-"`
Attributes []html.Attribute `json:"-"`
Translations []Translation `json:"-"` // will be deprecated
namespace string `json:"-"`
parent *Page `json:"-"`
Route string `json:"route"`
Name string `json:"name,omitempty"`
CacheStore string `json:"-"`
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:"-"`
Attributes []html.Attribute `json:"-"`
namespace string `json:"-"`
parent *Page `json:"-"`
}
// BuildContext is the struct for the build context

View file

@ -205,7 +205,7 @@ func (page *Page) Build(globalCtx *core.GlobalBuildContext, option *core.BuildOp
}
// Save the locale files
err = page.writeLocaleFiles(option.Data)
err = page.writeLocaleFiles(ctx, option.Data)
if err != nil {
return warnings, err
}
@ -272,7 +272,7 @@ func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *c
}
// Save the locale files
err = page.writeLocaleFiles(option.Data)
err = page.writeLocaleFiles(ctx, option.Data)
if err != nil {
return warnings, err
}
@ -415,10 +415,14 @@ func (page *Page) locale(name string) core.Locale {
return locale
}
func (page *Page) writeLocaleFiles(data map[string]interface{}) error {
func (page *Page) writeLocaleFiles(ctx *core.BuildContext, data map[string]interface{}) error {
// No translations
if len(page.Page.Translations) == 0 {
if ctx == nil {
return nil
}
translations := ctx.GetTranslations()
if len(translations) == 0 {
return nil
}
@ -428,7 +432,7 @@ func (page *Page) writeLocaleFiles(data map[string]interface{}) error {
// Init Data
keys := map[string]string{}
messages := map[string]string{}
for _, t := range page.Page.Translations {
for _, t := range translations {
keys[t.Key] = t.Message
messages[t.Message] = t.Message
}

View file

@ -279,12 +279,11 @@ 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,
Translations: []core.Translation{},
Route: route,
TemplateID: tmpl.ID,
SuiID: tmpl.local.ID,
Path: filepath.Join(tmpl.Root, route),
Name: name,
Codes: core.SourceCodes{
HTML: core.Source{File: fmt.Sprintf("%s.html", name)},
CSS: core.Source{File: fmt.Sprintf("%s.css", name)},
@ -350,12 +349,11 @@ 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,
Translations: []core.Translation{},
Route: route,
Path: path,
Name: name,
TemplateID: tmpl.ID,
SuiID: tmpl.local.ID,
Codes: core.SourceCodes{
HTML: core.Source{File: fmt.Sprintf("%s%s", name, filepath.Ext(file))},
CSS: core.Source{File: fmt.Sprintf("%s.css", name)},