[add] sui.trans.all and sui.trans.page to support automatic translation
This commit is contained in:
parent
1ed1973c7d
commit
ef8b8027cf
9 changed files with 370 additions and 23 deletions
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue