[add] sui.trans.all and sui.trans.page to support automatic translation

This commit is contained in:
Max 2024-07-15 09:24:44 +08:00
parent 1ed1973c7d
commit ef8b8027cf
9 changed files with 370 additions and 23 deletions

View file

@ -59,6 +59,9 @@ func init() {
"build.all": BuildAll,
"build.page": BuildPage,
"trans.all": TransAll,
"trans.page": TransPage,
"sync.assetfile": SyncAssetFile, // Will be deprecated or change in the future
// Will be deprecated or change in the future
@ -1018,6 +1021,89 @@ func BuildPage(process *process.Process) interface{} {
return nil
}
// TransAll handle the render page request
func TransAll(process *process.Process) interface{} {
process.ValidateArgNums(3)
sui := get(process)
templateID := process.ArgsString(1)
option := process.ArgsMap(2, map[string]interface{}{})
ssr := true
if v, ok := option["ssr"].(bool); ok {
ssr = v
}
assetRoot := ""
if v, ok := option["asset_root"].(string); ok {
assetRoot = v
}
data := map[string]interface{}{}
if v, ok := option["data"].(map[string]interface{}); ok {
data = v
}
tmpl, err := sui.GetTemplate(templateID)
if err != nil {
exception.New(err.Error(), 500).Throw()
}
warnings, err := tmpl.Trans(&core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data})
if err != nil {
exception.New(err.Error(), 500).Throw()
}
if warnings != nil && len(warnings) > 0 {
return warnings
}
return nil
}
// TransPage handle the render page request
func TransPage(process *process.Process) interface{} {
process.ValidateArgNums(4)
sui := get(process)
templateID := process.ArgsString(1)
route := route(process, 2)
option := process.ArgsMap(3, map[string]interface{}{})
ssr := true
if v, ok := option["ssr"].(bool); ok {
ssr = v
}
assetRoot := ""
if v, ok := option["asset_root"].(string); ok {
assetRoot = v
}
tmpl, err := sui.GetTemplate(templateID)
if err != nil {
exception.New(err.Error(), 500).Throw()
}
page, err := tmpl.Page(route)
if err != nil {
exception.New(err.Error(), 500).Throw()
}
err = page.Load()
if err != nil {
exception.New(err.Error(), 500).Throw()
}
data := process.ArgsMap(5, map[string]interface{}{})
warnings, err := page.Trans(nil, &core.BuildOption{SSR: ssr, AssetRoot: assetRoot, Data: data})
if err != nil {
exception.New(err.Error(), 500).Throw()
}
if warnings != nil && len(warnings) > 0 {
return warnings
}
return nil
}
// get the sui
func get(process *process.Process) core.SUI {
sui, has := core.SUIs[process.ArgsString(0)]

View file

@ -87,10 +87,13 @@ func TestTemplateLocaleGet(t *testing.T) {
}
assert.IsType(t, []core.SelectOption{}, res)
assert.Equal(t, 3, len(res.([]core.SelectOption)))
assert.Equal(t, "ja-jp", res.([]core.SelectOption)[0].Value)
assert.Equal(t, 4, len(res.([]core.SelectOption)))
assert.Equal(t, "en-us", res.([]core.SelectOption)[0].Value)
assert.True(t, res.([]core.SelectOption)[0].Default)
assert.Equal(t, "zh-cn", res.([]core.SelectOption)[1].Value)
assert.Equal(t, "zh-hk", res.([]core.SelectOption)[2].Value)
assert.Equal(t, "ja-jp", res.([]core.SelectOption)[3].Value)
}
func TestTemplateThemeGet(t *testing.T) {
@ -723,6 +726,42 @@ func TestBuildPage(t *testing.T) {
assert.Nil(t, res)
}
func TestTransAll(t *testing.T) {
prepare(t)
defer clean()
// test demo
p, err := process.Of("sui.trans.all", "test", "advanced", map[string]interface{}{"ssr": true})
if err != nil {
t.Fatal(err)
}
res, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Nil(t, res)
}
func TestTransPage(t *testing.T) {
prepare(t)
defer clean()
// test demo
p, err := process.Of("sui.trans.page", "test", "advanced", "/i18n", map[string]interface{}{"ssr": true})
if err != nil {
t.Fatal(err)
}
res, err := p.Exec()
if err != nil {
t.Fatal(err)
}
assert.Nil(t, res)
}
func TestSyncAssetFile(t *testing.T) {
prepare(t)
defer clean()

View file

@ -65,6 +65,8 @@ type ITemplate interface {
ExecBeforeBuildScripts() []TemplateScirptResult
ExecAfterBuildScripts() []TemplateScirptResult
Trans(option *BuildOption) ([]string, error)
}
// IPage is the interface for the page
@ -94,6 +96,8 @@ type IPage interface {
Build(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error)
BuildAsComponent(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error)
Trans(globalCtx *GlobalBuildContext, option *BuildOption) ([]string, error)
}
// IBlock is the interface for the block

View file

@ -186,6 +186,7 @@ type Template struct {
Document []byte `json:"-"`
GlobalData []byte `json:"-"`
Scripts *TemplateScirpts `json:"scripts,omitempty"`
Translator string `json:"translator,omitempty"`
}
// TemplateScirpts is the struct for the template scripts
@ -217,8 +218,9 @@ type Theme struct {
// SelectOption is the struct for the select option
type SelectOption struct {
Label string `json:"label"`
Value string `json:"value"`
Label string `json:"label"`
Value string `json:"value"`
Default bool `json:"default"`
}
// Asset is the struct for the asset

View file

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/yaoapp/gou/application"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/sui/core"
"gopkg.in/yaml.v3"
@ -127,6 +128,37 @@ func (tmpl *Template) Build(option *core.BuildOption) ([]string, error) {
return warnings, err
}
// Trans the template
func (tmpl *Template) Trans(option *core.BuildOption) ([]string, error) {
var err error
warnings := []string{}
ctx := core.NewGlobalBuildContext()
pages, err := tmpl.Pages()
if err != nil {
return warnings, err
}
// loaed pages
for _, page := range pages {
err := page.Load()
if err != nil {
return warnings, err
}
messages, err := page.Trans(ctx, option)
if err != nil {
return warnings, err
}
if len(messages) > 0 {
warnings = append(warnings, messages...)
}
}
return warnings, nil
}
// SyncAssetFile sync the assets
func (tmpl *Template) SyncAssetFile(file string, option *core.BuildOption) error {
@ -309,6 +341,25 @@ func (page *Page) BuildAsComponent(globalCtx *core.GlobalBuildContext, option *c
return warnings, err
}
// Trans the page
func (page *Page) Trans(globalCtx *core.GlobalBuildContext, option *core.BuildOption) ([]string, error) {
warnings := []string{}
ctx := core.NewBuildContext(globalCtx)
_, messages, err := page.Page.CompileAsComponent(ctx, option)
if err != nil {
return warnings, err
}
if len(messages) > 0 {
warnings = append(warnings, messages...)
}
// Tranlate the locale files
err = page.writeLocaleSource(ctx)
return warnings, err
}
func (page *Page) publicFile(data map[string]interface{}) string {
root, err := page.tmpl.local.DSL.PublicRoot(data)
if err != nil {
@ -328,6 +379,9 @@ func (page *Page) localeFiles(data map[string]interface{}) map[string]string {
roots := map[string]string{}
locales := page.tmpl.Locales()
for _, locale := range locales {
if locale.Default {
continue
}
target := filepath.Join("/", "public", root, ".locales", locale.Value, fmt.Sprintf("%s.yml", page.Route))
roots[locale.Value] = target
}
@ -365,14 +419,13 @@ func (page *Page) localeGlobal(name string) core.Locale {
return global
}
func (page *Page) locale(name string) core.Locale {
func (page *Page) locale(name string, pageOnly ...bool) 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
}
@ -399,22 +452,95 @@ func (page *Page) locale(name string) core.Locale {
return global
}
// Merge the global
for key, message := range global.Keys {
if _, ok := locale.Keys[key]; !ok {
locale.Keys[key] = message
}
}
if len(pageOnly) == 0 || !pageOnly[0] {
for key, message := range global.Messages {
if _, ok := locale.Messages[key]; !ok {
locale.Messages[key] = message
// 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) writeLocaleSource(ctx *core.BuildContext) error {
locales := page.tmpl.Locales()
translations := ctx.GetTranslations()
for _, lc := range locales {
if lc.Default {
continue
}
locale := page.locale(lc.Value, true)
for _, t := range translations {
message := t.Message
// Match the key
if _, has := locale.Messages[message]; has {
message = locale.Messages[message]
}
locale.Keys[t.Key] = message
msg, has := locale.Messages[t.Message]
if has && msg != t.Message {
continue
}
locale.Messages[t.Message] = t.Message
}
// Call the hook
var keys any = locale.Keys
var messages any = locale.Messages
if page.tmpl.Translator != "" {
p, err := process.Of(page.tmpl.Translator, lc.Value, locale, page.Route, page.TemplateID)
if err != nil {
return err
}
res, err := p.Exec()
if err != nil {
return err
}
pres, ok := res.(map[string]interface{})
if !ok {
return fmt.Errorf("The translator %s should return a locale", page.tmpl.Translator)
}
keys = pres["keys"]
messages = pres["messages"]
}
if keys == nil && messages == nil {
return nil
}
// Save to file
file := filepath.Join(page.tmpl.Root, "__locales", lc.Value, fmt.Sprintf("%s.yml", page.Route))
content, err := yaml.Marshal(map[string]interface{}{
"keys": keys,
"messages": messages,
})
if err != nil {
return err
}
_, err = page.tmpl.local.fs.WriteFile(file, content, 0644)
if err != nil {
return err
}
}
return nil
}
func (page *Page) writeLocaleFiles(ctx *core.BuildContext, data map[string]interface{}) error {
if ctx == nil {

View file

@ -198,3 +198,74 @@ func TestPageBuildAsComponent(t *testing.T) {
assert.Contains(t, string(content), "function Init()")
assert.Contains(t, string(content), `type="flowbite-edit-select"`)
}
func TestPageTrans(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Test.GetTemplate("advanced")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
root := application.App.Root()
path := filepath.Join(root, "data", tmpl.GetRoot(), "__locales")
// Remove files and directories in Public directory if exists
err = os.RemoveAll(path)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("RemoveAll error: %v", err)
}
page, err := tmpl.Page("/i18n")
if err != nil {
t.Fatalf("Page error: %v", err)
}
warnings, err := page.Trans(nil, &core.BuildOption{SSR: true, AssetRoot: "/unit-test/assets"})
if err != nil {
t.Fatalf("Page Build error: %v", err)
}
assert.DirExists(t, path)
assert.DirExists(t, filepath.Join(path, "zh-cn"))
assert.DirExists(t, filepath.Join(path, "zh-hk"))
assert.DirExists(t, filepath.Join(path, "ja-jp"))
assert.FileExists(t, filepath.Join(path, "zh-cn", "i18n.yml"))
assert.FileExists(t, filepath.Join(path, "zh-hk", "i18n.yml"))
assert.FileExists(t, filepath.Join(path, "ja-jp", "i18n.yml"))
assert.Len(t, warnings, 0)
}
func TestTemplateTrans(t *testing.T) {
tests := prepare(t)
defer clean()
tmpl, err := tests.Test.GetTemplate("advanced")
if err != nil {
t.Fatalf("GetTemplate error: %v", err)
}
root := application.App.Root()
path := filepath.Join(root, "data", tmpl.GetRoot(), "__locales")
// Remove files and directories in Public directory if exists
err = os.RemoveAll(path)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("RemoveAll error: %v", err)
}
warnings, err := tmpl.Trans(&core.BuildOption{SSR: true})
if err != nil {
t.Fatalf("Components error: %v", err)
}
assert.DirExists(t, path)
assert.DirExists(t, filepath.Join(path, "zh-cn"))
assert.DirExists(t, filepath.Join(path, "zh-hk"))
assert.DirExists(t, filepath.Join(path, "ja-jp"))
assert.FileExists(t, filepath.Join(path, "zh-cn", "i18n.yml"))
assert.FileExists(t, filepath.Join(path, "zh-hk", "i18n.yml"))
assert.FileExists(t, filepath.Join(path, "ja-jp", "i18n.yml"))
assert.Len(t, warnings, 0)
}

View file

@ -31,7 +31,7 @@ func TestGetTemplates(t *testing.T) {
assert.Equal(t, "advanced", testTmpls[0].(*Template).ID)
assert.Equal(t, "The advanced template", testTmpls[0].(*Template).Name)
assert.Len(t, testTmpls[0].Themes(), 2)
assert.Len(t, testTmpls[0].Locales(), 3)
assert.Len(t, testTmpls[0].Locales(), 4)
assert.Len(t, testTmpls[0].(*Template).Template.Themes, 2)
assert.Len(t, testTmpls[0].(*Template).Template.Locales, 4)
@ -57,7 +57,7 @@ func TestGetTemplates(t *testing.T) {
assert.Equal(t, "default", webTmpls[0].(*Template).ID)
assert.Equal(t, "Yao Startup Webapp", webTmpls[0].(*Template).Name)
assert.Len(t, webTmpls[0].Themes(), 2)
assert.Len(t, webTmpls[0].Locales(), 3)
assert.Len(t, webTmpls[0].Locales(), 4)
assert.Len(t, webTmpls[0].(*Template).Template.Themes, 2)
assert.Len(t, webTmpls[0].(*Template).Template.Locales, 2)
}

View file

@ -168,20 +168,36 @@ func (tmpl *Template) Locales() []core.SelectOption {
return tmpl.locales
}
// Defined the support locales
supportLocales := []core.SelectOption{}
localeMap := map[string]bool{}
locales := tmpl.Template.Locales
for _, locale := range locales {
if localeMap[locale.Value] {
continue
}
localeMap[locale.Value] = true
supportLocales = append(supportLocales, locale)
}
path := filepath.Join(tmpl.Root, "__locales")
if !tmpl.local.fs.IsDir(path) {
return nil
return supportLocales
}
dirs, err := tmpl.local.fs.ReadDir(path, false)
if err != nil {
return nil
return supportLocales
}
// Get the support locales
for _, dir := range dirs {
locale := filepath.Base(dir)
if localeMap[locale] {
continue
}
label := language.Make(locale).String()
localeMap[locale] = true
supportLocales = append(supportLocales, core.SelectOption{
Value: locale,
Label: label,

View file

@ -41,14 +41,17 @@ func TestTemplateLocales(t *testing.T) {
t.Fatalf("Locales error: %v", len(locales))
}
assert.Equal(t, "ja-JP", locales[0].Label)
assert.Equal(t, "ja-jp", locales[0].Value)
assert.Equal(t, "English", locales[0].Label)
assert.Equal(t, "en-us", locales[0].Value)
assert.Equal(t, "zh-CN", locales[1].Label)
assert.Equal(t, "简体中文", locales[1].Label)
assert.Equal(t, "zh-cn", locales[1].Value)
assert.Equal(t, "zh-HK", locales[2].Label)
assert.Equal(t, "繁體中文", locales[2].Label)
assert.Equal(t, "zh-hk", locales[2].Value)
assert.Equal(t, "日本語", locales[3].Label)
assert.Equal(t, "ja-jp", locales[3].Value)
}
func TestTemplateAsset(t *testing.T) {