From 67a618eaf79114384d1c68400d7c14471d8f3520 Mon Sep 17 00:00:00 2001 From: rot1024 Date: Wed, 9 Feb 2022 20:16:11 +0900 Subject: [PATCH] feat: support 3rd party plugin translation (#109) * add i18n.String.WithDefault, rename i18n.String.Copy * add id constructors to property * make schema group fields nil * change schema id pattern * fix test * refactor manifest translation * support 3rd party plugin translation --- internal/infrastructure/fs/common.go | 26 -- internal/infrastructure/fs/file_test.go | 20 +- internal/infrastructure/fs/plugin.go | 63 ++++- .../infrastructure/fs/plugin_repository.go | 2 +- internal/infrastructure/fs/plugin_test.go | 39 +++ internal/infrastructure/fs/property_schema.go | 2 +- pkg/builtin/main.go | 14 +- pkg/i18n/string.go | 34 ++- pkg/i18n/string_test.go | 161 +++++++++++-- pkg/id/property_schema.go | 4 +- pkg/id/property_schema_test.go | 32 ++- pkg/plugin/builder.go | 4 +- pkg/plugin/extension.go | 12 +- pkg/plugin/extension_builder.go | 4 +- pkg/plugin/manifest/convert.go | 133 ++++++---- pkg/plugin/manifest/convert_test.go | 194 ++++++++++----- pkg/plugin/manifest/diff_test.go | 2 +- pkg/plugin/manifest/parser.go | 12 +- pkg/plugin/manifest/parser_test.go | 8 +- pkg/plugin/manifest/parser_translation.go | 139 +---------- .../manifest/parser_translation_test.go | 68 +----- pkg/plugin/manifest/schema_translation.go | 228 ++++++++++++++++++ .../manifest/schema_translation_test.go | 188 +++++++++++++++ .../manifest/testdata/translation_merge.yml | 34 --- pkg/plugin/plugin.go | 22 +- pkg/plugin/plugin_test.go | 12 - pkg/plugin/pluginpack/package.go | 39 ++- pkg/plugin/pluginpack/package_test.go | 18 +- pkg/plugin/pluginpack/testdata/test.zip | Bin 789 -> 1804 bytes .../pluginpack/testdata/test/reearth_ja.yml | 1 + .../testdata/test/reearth_zh-CN.yml | 1 + pkg/property/id.go | 2 + pkg/property/schema_field.go | 12 +- pkg/property/schema_field_builder.go | 4 +- pkg/property/schema_group.go | 4 +- pkg/property/schema_group_builder.go | 7 +- 36 files changed, 1062 insertions(+), 483 deletions(-) create mode 100644 internal/infrastructure/fs/plugin_test.go create mode 100644 pkg/plugin/manifest/schema_translation_test.go delete mode 100644 pkg/plugin/manifest/testdata/translation_merge.yml create mode 100644 pkg/plugin/pluginpack/testdata/test/reearth_ja.yml create mode 100644 pkg/plugin/pluginpack/testdata/test/reearth_zh-CN.yml diff --git a/internal/infrastructure/fs/common.go b/internal/infrastructure/fs/common.go index c4bfccff..0eb93b45 100644 --- a/internal/infrastructure/fs/common.go +++ b/internal/infrastructure/fs/common.go @@ -1,34 +1,8 @@ package fs -import ( - "path/filepath" - - "github.com/reearth/reearth-backend/pkg/id" - "github.com/reearth/reearth-backend/pkg/plugin/manifest" - "github.com/reearth/reearth-backend/pkg/rerror" - "github.com/spf13/afero" -) - const ( assetDir = "assets" pluginDir = "plugins" publishedDir = "published" manifestFilePath = "reearth.yml" ) - -func readManifest(fs afero.Fs, pid id.PluginID) (*manifest.Manifest, error) { - f, err := fs.Open(filepath.Join(pluginDir, pid.String(), manifestFilePath)) - if err != nil { - return nil, rerror.ErrInternalBy(err) - } - defer func() { - _ = f.Close() - }() - - m, err := manifest.Parse(f, nil) - if err != nil { - return nil, err - } - - return m, nil -} diff --git a/internal/infrastructure/fs/file_test.go b/internal/infrastructure/fs/file_test.go index a57913d3..065a8bfa 100644 --- a/internal/infrastructure/fs/file_test.go +++ b/internal/infrastructure/fs/file_test.go @@ -249,15 +249,17 @@ func TestGetAssetFileURL(t *testing.T) { } func mockFs() afero.Fs { + files := map[string]string{ + "assets/xxx.txt": "hello", + "plugins/aaa~1.0.0/foo.js": "bar", + "published/s.json": "{}", + } + fs := afero.NewMemMapFs() - f, _ := fs.Create("assets/xxx.txt") - _, _ = f.WriteString("hello") - _ = f.Close() - f, _ = fs.Create("plugins/aaa~1.0.0/foo.js") - _, _ = f.WriteString("bar") - _ = f.Close() - f, _ = fs.Create("published/s.json") - _, _ = f.WriteString("{}") - _ = f.Close() + for name, content := range files { + f, _ := fs.Create(name) + _, _ = f.WriteString(content) + _ = f.Close() + } return fs } diff --git a/internal/infrastructure/fs/plugin.go b/internal/infrastructure/fs/plugin.go index a201b514..f5d4c568 100644 --- a/internal/infrastructure/fs/plugin.go +++ b/internal/infrastructure/fs/plugin.go @@ -3,10 +3,13 @@ package fs import ( "context" "errors" + "path/filepath" + "regexp" "github.com/reearth/reearth-backend/internal/usecase/repo" "github.com/reearth/reearth-backend/pkg/id" "github.com/reearth/reearth-backend/pkg/plugin" + "github.com/reearth/reearth-backend/pkg/plugin/manifest" "github.com/reearth/reearth-backend/pkg/rerror" "github.com/spf13/afero" ) @@ -22,7 +25,7 @@ func NewPlugin(fs afero.Fs) repo.Plugin { } func (r *pluginRepo) FindByID(ctx context.Context, pid id.PluginID, sids []id.SceneID) (*plugin.Plugin, error) { - m, err := readManifest(r.fs, pid) + m, err := readPluginManifest(r.fs, pid) if err != nil { return nil, err } @@ -54,3 +57,61 @@ func (r *pluginRepo) Save(ctx context.Context, p *plugin.Plugin) error { func (r *pluginRepo) Remove(ctx context.Context, pid id.PluginID) error { return rerror.ErrInternalBy(errors.New("read only")) } + +var translationFileNameRegexp = regexp.MustCompile(`reearth_([a-zA-Z]+(?:-[a-zA-Z]+)?).yml`) + +func readPluginManifest(fs afero.Fs, pid id.PluginID) (*manifest.Manifest, error) { + base := filepath.Join(pluginDir, pid.String()) + translationMap, err := readPluginTranslation(fs, base) + if err != nil { + return nil, err + } + + f, err := fs.Open(filepath.Join(base, manifestFilePath)) + if err != nil { + return nil, rerror.ErrInternalBy(err) + } + defer func() { + _ = f.Close() + }() + + m, err := manifest.Parse(f, nil, translationMap.TranslatedRef()) + if err != nil { + return nil, err + } + + return m, nil +} + +func readPluginTranslation(fs afero.Fs, base string) (manifest.TranslationMap, error) { + d, err := afero.ReadDir(fs, base) + if err != nil { + return nil, rerror.ErrInternalBy(err) + } + + translationMap := manifest.TranslationMap{} + for _, e := range d { + if e.IsDir() { + continue + } + name := e.Name() + lang := translationFileNameRegexp.FindStringSubmatch(name) + if len(lang) == 0 { + continue + } + langfile, err := fs.Open(filepath.Join(base, name)) + if err != nil { + return nil, rerror.ErrInternalBy(err) + } + defer func() { + _ = langfile.Close() + }() + t, err := manifest.ParseTranslation(langfile) + if err != nil { + return nil, err + } + translationMap[lang[1]] = t + } + + return translationMap, nil +} diff --git a/internal/infrastructure/fs/plugin_repository.go b/internal/infrastructure/fs/plugin_repository.go index aae3a63e..04c8083a 100644 --- a/internal/infrastructure/fs/plugin_repository.go +++ b/internal/infrastructure/fs/plugin_repository.go @@ -26,5 +26,5 @@ func (r *pluginRepository) Data(ctx context.Context, id id.PluginID) (file.Itera } func (r *pluginRepository) Manifest(ctx context.Context, id id.PluginID) (*manifest.Manifest, error) { - return readManifest(r.fs, id) + return readPluginManifest(r.fs, id) } diff --git a/internal/infrastructure/fs/plugin_test.go b/internal/infrastructure/fs/plugin_test.go new file mode 100644 index 00000000..67bd4cb8 --- /dev/null +++ b/internal/infrastructure/fs/plugin_test.go @@ -0,0 +1,39 @@ +package fs + +import ( + "context" + "testing" + + "github.com/reearth/reearth-backend/pkg/i18n" + "github.com/reearth/reearth-backend/pkg/plugin" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" +) + +func TestPlugin(t *testing.T) { + ctx := context.Background() + fs := NewPlugin(mockPluginFS()) + p, err := fs.FindByID(ctx, plugin.MustID("testplugin~1.0.0"), nil) + assert.NoError(t, err) + assert.Equal(t, plugin.New().ID(plugin.MustID("testplugin~1.0.0")).Name(i18n.String{ + "en": "testplugin", + "ja": "テストプラグイン", + "zh-CN": "测试插件", + }).MustBuild(), p) +} + +func mockPluginFS() afero.Fs { + files := map[string]string{ + "plugins/testplugin~1.0.0/reearth.yml": `{ "id": "testplugin", "version": "1.0.0", "name": "testplugin" }`, + "plugins/testplugin~1.0.0/reearth_ja.yml": `{ "name": "テストプラグイン" }`, + "plugins/testplugin~1.0.0/reearth_zh-CN.yml": `{ "name": "测试插件" }`, + } + + fs := afero.NewMemMapFs() + for name, content := range files { + f, _ := fs.Create(name) + _, _ = f.WriteString(content) + _ = f.Close() + } + return fs +} diff --git a/internal/infrastructure/fs/property_schema.go b/internal/infrastructure/fs/property_schema.go index 2ae587a7..d1d92fb6 100644 --- a/internal/infrastructure/fs/property_schema.go +++ b/internal/infrastructure/fs/property_schema.go @@ -22,7 +22,7 @@ func NewPropertySchema(fs afero.Fs) repo.PropertySchema { } func (r *propertySchema) FindByID(ctx context.Context, i id.PropertySchemaID) (*property.Schema, error) { - m, err := readManifest(r.fs, i.Plugin()) + m, err := readPluginManifest(r.fs, i.Plugin()) if err != nil { return nil, err } diff --git a/pkg/builtin/main.go b/pkg/builtin/main.go index e64c659c..b2979386 100644 --- a/pkg/builtin/main.go +++ b/pkg/builtin/main.go @@ -15,14 +15,16 @@ var pluginManifestJSON []byte //go:embed manifest_ja.yml var pluginManifestJSON_ja []byte -var pluginTranslationList = map[string]*manifest.TranslationRoot{"ja": manifest.MustParseTranslationFromBytes(pluginManifestJSON_ja)} -var pluginManifest = manifest.MergeManifestTranslation(manifest.MustParseSystemFromBytes(pluginManifestJSON, nil), pluginTranslationList) - -// MUST NOT CHANGE -var PropertySchemaIDVisualizerCesium = property.MustSchemaID("reearth/cesium") +var pluginTranslationList = manifest.TranslationMap{ + "ja": manifest.MustParseTranslationFromBytes(pluginManifestJSON_ja), +} +var pluginManifest = manifest.MustParseSystemFromBytes(pluginManifestJSON, nil, pluginTranslationList.TranslatedRef()) // MUST NOT CHANGE -var PropertySchemaIDInfobox = property.MustSchemaID("reearth/infobox") +var ( + PropertySchemaIDVisualizerCesium = property.MustSchemaID("reearth/cesium") + PropertySchemaIDInfobox = property.MustSchemaID("reearth/infobox") +) func GetPropertySchemaByVisualizer(v visualizer.Visualizer) *property.Schema { for _, p := range pluginManifest.ExtensionSchema { diff --git a/pkg/i18n/string.go b/pkg/i18n/string.go index f4bc4dde..410b84fe 100644 --- a/pkg/i18n/string.go +++ b/pkg/i18n/string.go @@ -1,12 +1,36 @@ package i18n +const DefaultLang = "en" + type String map[string]string // key should use BCP 47 representation func StringFrom(s string) String { if s == "" { + return String{} + } + return String{DefaultLang: s} +} + +func (s String) WithDefault(d string) String { + if s == nil && d == "" { return nil } - return String{"en": s} + + res := s.Clone() + if res == nil { + res = String{} + } + if d != "" { + res[DefaultLang] = d + } + return res +} + +func (s String) WithDefaultRef(d *string) String { + if d == nil { + return s.Clone() + } + return s.WithDefault(*d) } func (s String) Translated(lang ...string) string { @@ -21,8 +45,8 @@ func (s String) Translated(lang ...string) string { return s.String() } -func (s String) Copy() String { - if s == nil { +func (s String) Clone() String { + if len(s) == 0 { return nil } s2 := make(String, len(s)) @@ -36,13 +60,13 @@ func (s String) String() string { if s == nil { return "" } - return s["en"] + return s[DefaultLang] } func (s String) StringRef() *string { if s == nil { return nil } - st := s["en"] + st := s[DefaultLang] return &st } diff --git a/pkg/i18n/string_test.go b/pkg/i18n/string_test.go index 88903ed7..8aa6affc 100644 --- a/pkg/i18n/string_test.go +++ b/pkg/i18n/string_test.go @@ -1,7 +1,6 @@ package i18n import ( - "reflect" "testing" "github.com/stretchr/testify/assert" @@ -9,18 +8,18 @@ import ( func TestString_String(t *testing.T) { tests := []struct { - Name, ExpectedStr string - I18nString String + Name, Expected string + Target String }{ { - Name: "en string", - ExpectedStr: "foo", - I18nString: String{"en": "foo"}, + Name: "en string", + Expected: "foo", + Target: String{"en": "foo"}, }, { - Name: "nil string", - ExpectedStr: "", - I18nString: nil, + Name: "nil string", + Expected: "", + Target: nil, }, } @@ -28,7 +27,115 @@ func TestString_String(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.ExpectedStr, tc.I18nString.String()) + assert.Equal(t, tc.Expected, tc.Target.String()) + }) + } +} + +func TestString_WithDefault(t *testing.T) { + tests := []struct { + Name string + Target String + Input string + Expected String + }{ + { + Name: "ok", + Target: String{"en": "foo", "ja": "bar"}, + Input: "x", + Expected: String{"en": "x", "ja": "bar"}, + }, + { + Name: "empty default", + Target: String{"en": "foo"}, + Input: "", + Expected: String{"en": "foo"}, + }, + { + Name: "empty", + Target: String{}, + Input: "x", + Expected: String{"en": "x"}, + }, + { + Name: "empty string and empty default", + Target: String{}, + Input: "", + Expected: String{}, + }, + { + Name: "nil string", + Target: nil, + Input: "x", + Expected: String{"en": "x"}, + }, + { + Name: "nil string and empty default", + Target: nil, + Input: "", + Expected: nil, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.Expected, tc.Target.WithDefault(tc.Input)) + }) + } +} + +func TestString_WithDefaultRef(t *testing.T) { + tests := []struct { + Name string + Target String + Input *string + Expected String + }{ + { + Name: "ok", + Target: String{"en": "foo", "ja": "bar"}, + Input: sr("x"), + Expected: String{"en": "x", "ja": "bar"}, + }, + { + Name: "nil default", + Target: String{"en": "foo", "ja": "bar"}, + Input: nil, + Expected: String{"en": "foo", "ja": "bar"}, + }, + { + Name: "empty default", + Target: String{"en": "foo"}, + Input: sr(""), + Expected: String{"en": "foo"}, + }, + { + Name: "empty", + Target: String{}, + Input: sr("x"), + Expected: String{"en": "x"}, + }, + { + Name: "empty string and empty default", + Target: String{}, + Input: sr(""), + Expected: String{}, + }, + { + Name: "nil string", + Target: nil, + Input: sr("x"), + Expected: String{"en": "x"}, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.Expected, tc.Target.WithDefaultRef(tc.Input)) }) } } @@ -69,25 +176,28 @@ func TestStringTranslated(t *testing.T) { func TestStringFrom(t *testing.T) { assert.Equal(t, String{"en": "foo"}, StringFrom("foo")) - assert.Nil(t, String(nil), StringFrom("")) + assert.Equal(t, String{}, StringFrom("")) } -func TestStringCopy(t *testing.T) { +func TestString_Clone(t *testing.T) { tests := []struct { - Name string - SourceString String + Name string + Target, Expected String }{ { - Name: "String with content", - SourceString: String{"ja": "foo"}, + Name: "String with content", + Target: String{"ja": "foo"}, + Expected: String{"ja": "foo"}, }, { - Name: "empty String", - SourceString: String{}, + Name: "empty String", + Target: String{}, + Expected: nil, }, { - Name: "nil", - SourceString: nil, + Name: "nil", + Target: nil, + Expected: nil, }, } @@ -95,10 +205,9 @@ func TestStringCopy(t *testing.T) { tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() - assert.True(t, reflect.DeepEqual(tc.SourceString, tc.SourceString.Copy())) - if tc.SourceString == nil { - assert.Nil(t, tc.SourceString.Copy()) - } + res := tc.Target.Clone() + assert.Equal(t, tc.Expected, res) + assert.NotSame(t, tc.Target, res) }) } } @@ -133,3 +242,7 @@ func TestString_StringRef(t *testing.T) { }) } } + +func sr(s string) *string { + return &s +} diff --git a/pkg/id/property_schema.go b/pkg/id/property_schema.go index 91795e25..7fc59478 100644 --- a/pkg/id/property_schema.go +++ b/pkg/id/property_schema.go @@ -7,7 +7,7 @@ import ( const schemaSystemIDPrefix = "reearth" -var schemaNameRe = regexp.MustCompile("^[a-zA-Z0-9_-]+$") +var schemaIDRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9_-]*$|^@$") // PropertySchemaID is an ID for PropertySchema. type PropertySchemaID struct { @@ -18,7 +18,7 @@ type PropertySchemaID struct { // PropertySchemaIDFrom generates a new PropertySchemaID from a string. func PropertySchemaIDFrom(id string) (PropertySchemaID, error) { ids := strings.SplitN(id, "/", 2) - if len(ids) < 2 || !schemaNameRe.MatchString(ids[len(ids)-1]) { + if len(ids) < 2 || !schemaIDRe.MatchString(ids[len(ids)-1]) { return PropertySchemaID{}, ErrInvalidID } pid, err := PluginIDFrom(ids[0]) diff --git a/pkg/id/property_schema_test.go b/pkg/id/property_schema_test.go index 4f35382b..cb8f234f 100644 --- a/pkg/id/property_schema_test.go +++ b/pkg/id/property_schema_test.go @@ -20,7 +20,7 @@ func TestPropertySchemaIDFrom(t *testing.T) { } }{ { - name: "success:valid name", + name: "success", input: "test~1.0.0/Test_Test-01", expected: struct { result PropertySchemaID @@ -34,7 +34,21 @@ func TestPropertySchemaIDFrom(t *testing.T) { }, }, { - name: "fail:invalid name", + name: "success: @", + input: "test~1.0.0/@", + expected: struct { + result PropertySchemaID + err error + }{ + result: PropertySchemaID{ + plugin: MustPluginID("test~1.0.0"), + id: "@", + }, + err: nil, + }, + }, + { + name: "fail 1", input: "Test", expected: struct { result PropertySchemaID @@ -42,7 +56,7 @@ func TestPropertySchemaIDFrom(t *testing.T) { }{result: PropertySchemaID{}, err: ErrInvalidID}, }, { - name: "fail:invalid name", + name: "fail 2", input: "Test/+dsad", expected: struct { result PropertySchemaID @@ -50,8 +64,16 @@ func TestPropertySchemaIDFrom(t *testing.T) { }{result: PropertySchemaID{}, err: ErrInvalidID}, }, { - name: "fail:invalid name", - input: "Test/dsa d", + name: "fail 3", + input: "Test/-", + expected: struct { + result PropertySchemaID + err error + }{result: PropertySchemaID{}, err: ErrInvalidID}, + }, + { + name: "fail 4", + input: "Test/__", expected: struct { result PropertySchemaID err error diff --git a/pkg/plugin/builder.go b/pkg/plugin/builder.go index 7cd0ddd2..6fad423d 100644 --- a/pkg/plugin/builder.go +++ b/pkg/plugin/builder.go @@ -31,7 +31,7 @@ func (b *Builder) ID(id ID) *Builder { } func (b *Builder) Name(name i18n.String) *Builder { - b.p.name = name.Copy() + b.p.name = name.Clone() return b } @@ -41,7 +41,7 @@ func (b *Builder) Author(author string) *Builder { } func (b *Builder) Description(description i18n.String) *Builder { - b.p.description = description.Copy() + b.p.description = description.Clone() return b } diff --git a/pkg/plugin/extension.go b/pkg/plugin/extension.go index 199bf016..083b8ae1 100644 --- a/pkg/plugin/extension.go +++ b/pkg/plugin/extension.go @@ -40,11 +40,11 @@ func (w *Extension) Type() ExtensionType { } func (w *Extension) Name() i18n.String { - return w.name.Copy() + return w.name.Clone() } func (w *Extension) Description() i18n.String { - return w.description.Copy() + return w.description.Clone() } func (w *Extension) Icon() string { @@ -71,12 +71,12 @@ func (w *Extension) WidgetLayout() *WidgetLayout { } func (w *Extension) Rename(name i18n.String) { - w.name = name.Copy() + w.name = name.Clone() } func (w *Extension) SetDescription(des i18n.String) { - w.description = des.Copy() + w.description = des.Clone() } func (w *Extension) Clone() *Extension { @@ -86,8 +86,8 @@ func (w *Extension) Clone() *Extension { return &Extension{ id: w.id, extensionType: w.extensionType, - name: w.name.Copy(), - description: w.description.Copy(), + name: w.name.Clone(), + description: w.description.Clone(), icon: w.icon, schema: w.schema.Clone(), visualizer: w.visualizer, diff --git a/pkg/plugin/extension_builder.go b/pkg/plugin/extension_builder.go index bd8781ff..a28142b2 100644 --- a/pkg/plugin/extension_builder.go +++ b/pkg/plugin/extension_builder.go @@ -42,7 +42,7 @@ func (b *ExtensionBuilder) ID(id ExtensionID) *ExtensionBuilder { } func (b *ExtensionBuilder) Name(name i18n.String) *ExtensionBuilder { - b.p.name = name.Copy() + b.p.name = name.Clone() return b } @@ -52,7 +52,7 @@ func (b *ExtensionBuilder) Type(extensionType ExtensionType) *ExtensionBuilder { } func (b *ExtensionBuilder) Description(description i18n.String) *ExtensionBuilder { - b.p.description = description.Copy() + b.p.description = description.Clone() return b } diff --git a/pkg/plugin/manifest/convert.go b/pkg/plugin/manifest/convert.go index 9901c162..9883e1f0 100644 --- a/pkg/plugin/manifest/convert.go +++ b/pkg/plugin/manifest/convert.go @@ -13,7 +13,7 @@ import ( var errInvalidManifestWith = rerror.With(ErrInvalidManifest) -func (i *Root) manifest(sid *plugin.SceneID) (*Manifest, error) { +func (i *Root) manifest(sid *plugin.SceneID, tl *TranslatedRoot) (*Manifest, error) { var pid plugin.ID var err error if i.System && string(i.ID) == plugin.OfficialPluginID.Name() { @@ -27,7 +27,11 @@ func (i *Root) manifest(sid *plugin.SceneID) (*Manifest, error) { var pluginSchema *property.Schema if i.Schema != nil { - schema, err := i.Schema.schema(pid, "@") + var ts *TranslatedPropertySchema + if tl != nil { + ts = &tl.Schema + } + schema, err := i.Schema.schema(pid, "@", ts) if err != nil { return nil, errInvalidManifestWith(rerror.From("plugin property schema", err)) } @@ -42,7 +46,12 @@ func (i *Root) manifest(sid *plugin.SceneID) (*Manifest, error) { } for _, e := range i.Extensions { - extension, extensionSchema, err2 := e.extension(pid, i.System) + var te *TranslatedExtension + if tl != nil { + te = tl.Extensions[string(e.ID)] + } + + extension, extensionSchema, err2 := e.extension(pid, i.System, te) if err2 != nil { return nil, errInvalidManifestWith(rerror.From(fmt.Sprintf("ext (%s)", e.ID), err2)) } @@ -50,22 +59,27 @@ func (i *Root) manifest(sid *plugin.SceneID) (*Manifest, error) { extensionSchemas = append(extensionSchemas, extensionSchema) } - var author, desc, repository string + var author, repository string if i.Author != nil { author = *i.Author } - if i.Description != nil { - desc = *i.Description - } if i.Repository != nil { repository = *i.Repository } + var name, desc i18n.String + if tl != nil { + name = tl.Name + desc = tl.Description + } + name = name.WithDefault(i.Name) + desc = desc.WithDefaultRef(i.Description) + p, err := plugin.New(). ID(pid). - Name(i18n.StringFrom(i.Name)). + Name(name). Author(author). - Description(i18n.StringFrom(desc)). + Description(desc). RepositoryURL(repository). Schema(pluginSchema.IDRef()). Extensions(extensions). @@ -81,9 +95,13 @@ func (i *Root) manifest(sid *plugin.SceneID) (*Manifest, error) { }, nil } -func (i Extension) extension(pluginID plugin.ID, sys bool) (*plugin.Extension, *property.Schema, error) { +func (i Extension) extension(pluginID plugin.ID, sys bool, te *TranslatedExtension) (*plugin.Extension, *property.Schema, error) { eid := string(i.ID) - schema, err := i.Schema.schema(pluginID, eid) + var ts *TranslatedPropertySchema + if te != nil { + ts = &te.PropertySchema + } + schema, err := i.Schema.schema(pluginID, eid, ts) if err != nil { return nil, nil, rerror.From("property schema", err) } @@ -122,11 +140,8 @@ func (i Extension) extension(pluginID plugin.ID, sys bool) (*plugin.Extension, * return nil, nil, fmt.Errorf("invalid type: %s", i.Type) } - var desc, icon string + var icon string var singleOnly bool - if i.Description != nil { - desc = *i.Description - } if i.Icon != nil { icon = *i.Icon } @@ -134,10 +149,18 @@ func (i Extension) extension(pluginID plugin.ID, sys bool) (*plugin.Extension, * singleOnly = *i.SingleOnly } + var name, desc i18n.String + if te != nil { + name = te.Name + desc = te.Description + } + name = name.WithDefault(i.Name) + desc = desc.WithDefaultRef(i.Description) + ext, err := plugin.NewExtension(). ID(plugin.ExtensionID(eid)). - Name(i18n.StringFrom(i.Name)). - Description(i18n.StringFrom(desc)). + Name(name). + Description(desc). Visualizer(viz). Type(typ). SingleOnly(singleOnly). @@ -184,7 +207,7 @@ func (l *WidgetLayout) layout() *plugin.WidgetLayout { return plugin.NewWidgetLayout(horizontallyExtendable, verticallyExtendable, extended, l.Floating, dl).Ref() } -func (i *PropertySchema) schema(pluginID plugin.ID, idstr string) (*property.Schema, error) { +func (i *PropertySchema) schema(pluginID plugin.ID, idstr string, ts *TranslatedPropertySchema) (*property.Schema, error) { psid, err := property.SchemaIDFrom(pluginID.String() + "/" + idstr) if err != nil { return nil, fmt.Errorf("invalid id: %s", pluginID.String()+"/"+idstr) @@ -199,7 +222,12 @@ func (i *PropertySchema) schema(pluginID plugin.ID, idstr string) (*property.Sch // groups groups := make([]*property.SchemaGroup, 0, len(i.Groups)) for _, d := range i.Groups { - item, err := d.schemaGroup() + var tg *TranslatedPropertySchemaGroup + if ts != nil { + tg = (*ts)[string(d.ID)] + } + + item, err := d.schemaGroup(tg) if err != nil { return nil, rerror.From(fmt.Sprintf("item (%s)", d.ID), err) } @@ -243,28 +271,41 @@ func (p *PropertyPointer) pointer() *property.SchemaFieldPointer { } } -func (i PropertySchemaGroup) schemaGroup() (*property.SchemaGroup, error) { - title := i.Title +func (i PropertySchemaGroup) schemaGroup(tg *TranslatedPropertySchemaGroup) (*property.SchemaGroup, error) { + var title i18n.String + if tg != nil { + title = tg.Title.Clone() + } + title = title.WithDefault(i.Title) + var representativeField *property.FieldID if i.RepresentativeField != nil { representativeField = property.FieldID(*i.RepresentativeField).Ref() } // fields - fields := make([]*property.SchemaField, 0, len(i.Fields)) - for _, d := range i.Fields { - field, err := d.schemaField() - if err != nil { - return nil, rerror.From(fmt.Sprintf("field (%s)", d.ID), err) + var fields []*property.SchemaField + if len(i.Fields) > 0 { + fields = make([]*property.SchemaField, 0, len(i.Fields)) + for _, d := range i.Fields { + var tf *TranslatedPropertySchemaField + if tg != nil { + tf = tg.Fields[string(d.ID)] + } + + field, err := d.schemaField(tf) + if err != nil { + return nil, rerror.From(fmt.Sprintf("field (%s)", d.ID), err) + } + fields = append(fields, field) } - fields = append(fields, field) } return property.NewSchemaGroup(). ID(property.SchemaGroupID(i.ID)). IsList(i.List). Fields(fields). - Title(i18n.StringFrom(title)). + Title(title). RepresentativeField(representativeField). IsAvailableIf(i.AvailableIf.condition()). Build() @@ -280,19 +321,21 @@ func (o *PropertyCondition) condition() *property.Condition { } } -func (i PropertySchemaField) schemaField() (*property.SchemaField, error) { +func (i PropertySchemaField) schemaField(tf *TranslatedPropertySchemaField) (*property.SchemaField, error) { t := property.ValueType(i.Type) if !t.Valid() { return nil, fmt.Errorf("invalid value type: %s", i.Type) } - var title, desc, prefix, suffix string - if i.Title != nil { - title = *i.Title - } - if i.Description != nil { - desc = *i.Description + var title, desc i18n.String + if tf != nil { + title = tf.Title.Clone() + desc = tf.Description.Clone() } + title = title.WithDefaultRef(i.Title) + desc = desc.WithDefaultRef(i.Description) + + var prefix, suffix string if i.Prefix != nil { prefix = *i.Prefix } @@ -307,14 +350,19 @@ func (i PropertySchemaField) schemaField() (*property.SchemaField, error) { if c.Key == "" { continue } - choices = append(choices, *c.choice()) + + var t i18n.String + if tf != nil { + t = tf.Choices[c.Key] + } + choices = append(choices, c.choice(t)) } } f, err := property.NewSchemaField(). ID(property.FieldID(i.ID)). - Name(i18n.StringFrom(title)). - Description(i18n.StringFrom(desc)). + Name(title). + Description(desc). Type(t). Prefix(prefix). Suffix(suffix). @@ -331,13 +379,10 @@ func (i PropertySchemaField) schemaField() (*property.SchemaField, error) { return f, err } -func (c *Choice) choice() *property.SchemaFieldChoice { - if c == nil { - return nil - } - return &property.SchemaFieldChoice{ +func (c Choice) choice(t i18n.String) property.SchemaFieldChoice { + return property.SchemaFieldChoice{ Key: c.Key, - Title: i18n.StringFrom(c.Label), + Title: t.WithDefault(c.Label), Icon: c.Icon, } } diff --git a/pkg/plugin/manifest/convert_test.go b/pkg/plugin/manifest/convert_test.go index 54b42d22..c8214095 100644 --- a/pkg/plugin/manifest/convert_test.go +++ b/pkg/plugin/manifest/convert_test.go @@ -20,17 +20,18 @@ func TestToValue(t *testing.T) { func TestChoice(t *testing.T) { tests := []struct { name string - ch *Choice - expected *property.SchemaFieldChoice + ch Choice + tc i18n.String + expected property.SchemaFieldChoice }{ { name: "success", - ch: &Choice{ + ch: Choice{ Icon: "aaa", Key: "nnn", Label: "vvv", }, - expected: &property.SchemaFieldChoice{ + expected: property.SchemaFieldChoice{ Key: "nnn", Title: i18n.StringFrom("vvv"), Icon: "aaa", @@ -42,10 +43,9 @@ func TestChoice(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, *tt.expected, *tt.ch.choice()) + assert.Equal(t, tt.expected, tt.ch.choice(tt.tc)) }) } - } func TestManifest(t *testing.T) { @@ -58,7 +58,9 @@ func TestManifest(t *testing.T) { tests := []struct { name string root *Root + scene *plugin.SceneID expected *Manifest + tl *TranslatedRoot err string }{ { @@ -72,27 +74,53 @@ func TestManifest(t *testing.T) { Description: nil, ID: "cesium", Name: "", - Schema: nil, Type: "visualizer", Visualizer: &cesium, }}, Repository: &r, System: true, Version: "1.1.1", + Schema: &PropertySchema{ + Groups: []PropertySchemaGroup{ + {ID: "default"}, + }, + }, + }, + tl: &TranslatedRoot{ + Name: i18n.String{"ja": "A"}, + Description: i18n.String{"ja": "B"}, + Extensions: map[string]*TranslatedExtension{"cesium": {Name: i18n.String{"ja": "セジウム"}}}, + Schema: TranslatedPropertySchema{"default": {Title: i18n.String{"ja": "デフォルト"}}}, }, expected: &Manifest{ Plugin: plugin.New(). ID(plugin.OfficialPluginID). - Name(i18n.StringFrom("aaa")). + Name(i18n.String{"en": "aaa", "ja": "A"}). + Author(a). + RepositoryURL(r). + Description(i18n.String{"en": d, "ja": "B"}). + Schema(property.MustSchemaIDFromExtension(plugin.OfficialPluginID, "@").Ref()). Extensions([]*plugin.Extension{ plugin.NewExtension(). ID("cesium"). + Name(i18n.String{"ja": "セジウム"}). Visualizer("cesium"). Type("visualizer"). - System(true).MustBuild(), + Schema(property.MustSchemaIDFromExtension(plugin.OfficialPluginID, "cesium")). + System(true). + MustBuild(), }).MustBuild(), - ExtensionSchema: nil, - Schema: nil, + ExtensionSchema: property.SchemaList{ + property.NewSchema(). + ID(property.MustSchemaIDFromExtension(plugin.OfficialPluginID, "cesium")). + MustBuild(), + }, + Schema: property.NewSchema(). + ID(property.MustSchemaIDFromExtension(plugin.OfficialPluginID, "@")). + Groups(property.NewSchemaGroupList([]*property.SchemaGroup{ + property.NewSchemaGroup().ID("default").Title(i18n.String{"ja": "デフォルト"}).MustBuild(), + })). + MustBuild(), }, }, { @@ -103,9 +131,7 @@ func TestManifest(t *testing.T) { System: true, }, expected: &Manifest{ - Plugin: plugin.New().ID(plugin.OfficialPluginID).Name(i18n.StringFrom("reearth")).MustBuild(), - ExtensionSchema: nil, - Schema: nil, + Plugin: plugin.New().ID(plugin.OfficialPluginID).Name(i18n.StringFrom("reearth")).MustBuild(), }, }, { @@ -166,13 +192,13 @@ func TestManifest(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - m, err := tt.root.manifest(nil) + m, err := tt.root.manifest(tt.scene, tt.tl) if tt.err == "" { - assert.Equal(t, tt.expected.Plugin.ID(), m.Plugin.ID()) - assert.Equal(t, tt.expected.Plugin.Name(), m.Plugin.Name()) - assert.Equal(t, len(tt.expected.Plugin.Extensions()), len(m.Plugin.Extensions())) + assert.Equal(t, tt.expected, m) + assert.NoError(t, err) } else { - assert.Equal(t, tt.err, err.Error()) + assert.Nil(t, m) + assert.EqualError(t, err, tt.err) } }) } @@ -189,6 +215,7 @@ func TestExtension(t *testing.T) { name string ext Extension sys bool + tl *TranslatedExtension pid plugin.ID expectedPE *plugin.Extension expectedPS *property.Schema @@ -201,24 +228,38 @@ func TestExtension(t *testing.T) { ID: "cesium", Name: "Cesium", Icon: &i, - Schema: nil, - Type: "visualizer", - Visualizer: &cesium, + Schema: &PropertySchema{ + Groups: []PropertySchemaGroup{ + {ID: "default"}, + }, + }, + Type: "visualizer", + Visualizer: &cesium, }, sys: true, pid: plugin.OfficialPluginID, + tl: &TranslatedExtension{ + Name: i18n.String{"ja": "セジウム"}, + Description: i18n.String{"ja": "DDD"}, + PropertySchema: TranslatedPropertySchema{ + "default": {Title: i18n.String{"ja": "デフォルト"}}, + }, + }, expectedPE: plugin.NewExtension(). ID("cesium"). - Name(i18n.StringFrom("Cesium")). + Name(i18n.String{"en": "Cesium", "ja": "セジウム"}). Visualizer("cesium"). Type(plugin.ExtensionTypeVisualizer). System(true). - Description(i18n.StringFrom("ddd")). + Description(i18n.String{"en": "ddd", "ja": "DDD"}). Schema(property.MustSchemaID("reearth/cesium")). Icon(i). MustBuild(), expectedPS: property.NewSchema(). ID(property.MustSchemaID("reearth/cesium")). + Groups(property.NewSchemaGroupList([]*property.SchemaGroup{ + property.NewSchemaGroup().ID("default").Title(i18n.String{"ja": "デフォルト"}).MustBuild(), + })). MustBuild(), }, { @@ -399,12 +440,15 @@ func TestExtension(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - pe, ps, err := tt.ext.extension(tt.pid, tt.sys) + pe, ps, err := tt.ext.extension(tt.pid, tt.sys, tt.tl) if tt.err == "" { assert.Equal(t, tt.expectedPE, pe) assert.Equal(t, tt.expectedPS, ps) + assert.Nil(t, err) } else { - assert.Equal(t, tt.err, err.Error()) + assert.EqualError(t, err, tt.err) + assert.Nil(t, pe) + assert.Nil(t, ps) } }) } @@ -540,12 +584,13 @@ func TestSchema(t *testing.T) { name, psid string ps *PropertySchema pid plugin.ID + tl *TranslatedPropertySchema expected *property.Schema err string }{ { name: "fail invalid id", - psid: "@", + psid: "~", ps: &PropertySchema{ Groups: nil, Linkable: nil, @@ -553,7 +598,7 @@ func TestSchema(t *testing.T) { }, pid: plugin.MustID("aaa~1.1.1"), expected: nil, - err: "invalid id: aaa~1.1.1/@", + err: "invalid id: aaa~1.1.1/~", }, { name: "success nil PropertySchema", @@ -563,7 +608,7 @@ func TestSchema(t *testing.T) { expected: property.NewSchema().ID(property.MustSchemaID("reearth/marker")).MustBuild(), }, { - name: "success ", + name: "success", psid: "marker", ps: &PropertySchema{ Groups: []PropertySchemaGroup{{ @@ -590,6 +635,9 @@ func TestSchema(t *testing.T) { Linkable: nil, Version: 0, }, + tl: &TranslatedPropertySchema{ + "default": {Title: i18n.String{"ja": "マーカー"}}, + }, pid: plugin.OfficialPluginID, expected: property. NewSchema(). @@ -597,6 +645,7 @@ func TestSchema(t *testing.T) { Groups(property.NewSchemaGroupList([]*property.SchemaGroup{ property.NewSchemaGroup(). ID("default"). + Title(i18n.String{"en": "marker", "ja": "マーカー"}). Fields([]*property.SchemaField{ property.NewSchemaField(). ID("location"). @@ -613,17 +662,13 @@ func TestSchema(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - res, err := tt.ps.schema(tt.pid, tt.psid) + res, err := tt.ps.schema(tt.pid, tt.psid, tt.tl) if tt.err == "" { - assert.Equal(t, tt.expected.Groups().Len(), res.Groups().Len()) - assert.Equal(t, tt.expected.LinkableFields(), res.LinkableFields()) - assert.Equal(t, tt.expected.Version(), res.Version()) - if res.Groups().Len() > 0 { - exg := tt.expected.Groups().Group(res.Groups().Groups()[0].ID()) - assert.NotNil(t, exg) - } + assert.Equal(t, tt.expected, res) + assert.Nil(t, err) } else { - assert.Equal(t, tt.err, err.Error()) + assert.Nil(t, res) + assert.EqualError(t, err, tt.err) } }) } @@ -636,6 +681,7 @@ func TestSchemaGroup(t *testing.T) { tests := []struct { name string psg PropertySchemaGroup + tl *TranslatedPropertySchemaGroup expected *property.SchemaGroup err string }{ @@ -662,13 +708,21 @@ func TestSchemaGroup(t *testing.T) { List: false, Title: "marker", }, + tl: &TranslatedPropertySchemaGroup{ + Title: i18n.String{"ja": "マーカー"}, + Description: i18n.String{"ja": "説明"}, + Fields: map[string]*TranslatedPropertySchemaField{ + "location": {Title: i18n.String{"en": "x"}}, + }, + }, expected: property.NewSchemaGroup(). ID("default"). - Title(i18n.StringFrom(str)). + Title(i18n.String{"en": str, "ja": "マーカー"}). Fields([]*property.SchemaField{ property.NewSchemaField(). ID("location"). Type(property.ValueTypeLatLng). + Name(i18n.String{"en": "x"}). MustBuild(), }).MustBuild(), }, @@ -704,17 +758,13 @@ func TestSchemaGroup(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - res, err := tt.psg.schemaGroup() + res, err := tt.psg.schemaGroup(tt.tl) if tt.err == "" { - assert.Equal(t, tt.expected.Title().String(), res.Title().String()) - assert.Equal(t, tt.expected.Title(), res.Title()) - assert.Equal(t, len(tt.expected.Fields()), len(res.Fields())) - if len(res.Fields()) > 0 { - exf := res.Fields()[0] - assert.NotNil(t, tt.expected.Field(exf.ID())) - } + assert.Equal(t, tt.expected, res) + assert.Nil(t, err) } else { - assert.Equal(t, tt.err, err.Error()) + assert.Nil(t, res) + assert.EqualError(t, err, tt.err) } }) } @@ -726,11 +776,12 @@ func TestSchemaField(t *testing.T) { tests := []struct { name string psg PropertySchemaField + tl *TranslatedPropertySchemaField expected *property.SchemaField err error }{ { - name: "success name not nil", + name: "success", psg: PropertySchemaField{ AvailableIf: nil, Choices: nil, @@ -745,8 +796,18 @@ func TestSchemaField(t *testing.T) { Type: "string", UI: nil, }, - expected: property.NewSchemaField().ID("aaa").Name(i18n.StringFrom("xx")).Description(i18n.StringFrom("")).Type(property.ValueTypeString).MustBuild(), - err: nil, + tl: &TranslatedPropertySchemaField{ + Title: i18n.String{"en": "TITLE", "ja": "タイトル"}, + Description: i18n.String{"ja": "説明"}, + Choices: map[string]i18n.String{"A": {"en": "a"}}, + }, + expected: property.NewSchemaField(). + ID("aaa"). + Name(i18n.String{"en": str, "ja": "タイトル"}). + Description(i18n.String{"ja": "説明"}). + Type(property.ValueTypeString). + MustBuild(), + err: nil, }, { name: "success description not nil", @@ -764,8 +825,13 @@ func TestSchemaField(t *testing.T) { Type: "string", UI: nil, }, - expected: property.NewSchemaField().ID("aaa").Name(i18n.StringFrom("")).Description(i18n.StringFrom("xx")).Type(property.ValueTypeString).MustBuild(), - err: nil, + expected: property.NewSchemaField(). + ID("aaa"). + Name(i18n.StringFrom("")). + Description(i18n.StringFrom("xx")). + Type(property.ValueTypeString). + MustBuild(), + err: nil, }, { name: "success prefix not nil", @@ -827,6 +893,9 @@ func TestSchemaField(t *testing.T) { Key: "nnn", Label: "vvv", }, + { + Key: "z", + }, }, DefaultValue: nil, Description: nil, @@ -839,14 +908,21 @@ func TestSchemaField(t *testing.T) { Type: "string", UI: nil, }, + tl: &TranslatedPropertySchemaField{ + Choices: map[string]i18n.String{"nnn": {"ja": "a"}, "z": {"en": "Z"}}, + }, expected: property.NewSchemaField(). ID("aaa"). Choices([]property.SchemaFieldChoice{ { Key: "nnn", - Title: i18n.StringFrom("vvv"), + Title: i18n.String{"en": "vvv", "ja": "a"}, Icon: "aaa", }, + { + Key: "z", + Title: i18n.String{"en": "Z"}, + }, }). Type(property.ValueTypeString). Name(i18n.StringFrom("")). @@ -891,14 +967,12 @@ func TestSchemaField(t *testing.T) { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() - res, err := tt.psg.schemaField() + res, err := tt.psg.schemaField(tt.tl) if tt.err == nil { - assert.Equal(t, tt.expected.Title(), res.Title()) - assert.Equal(t, tt.expected.Description(), res.Description()) - assert.Equal(t, tt.expected.Suffix(), res.Suffix()) - assert.Equal(t, tt.expected.Prefix(), res.Prefix()) - assert.Equal(t, tt.expected.Choices(), res.Choices()) + assert.Equal(t, tt.expected, res) + assert.Nil(t, err) } else { + assert.Nil(t, res) assert.Equal(t, tt.err, rerror.Get(err).Err) } }) diff --git a/pkg/plugin/manifest/diff_test.go b/pkg/plugin/manifest/diff_test.go index d35b3589..9a0bd0f7 100644 --- a/pkg/plugin/manifest/diff_test.go +++ b/pkg/plugin/manifest/diff_test.go @@ -11,7 +11,7 @@ import ( func TestDiffFrom(t *testing.T) { oldp := plugin.MustID("aaaaaa~1.0.0") newp := plugin.MustID("aaaaaa~1.1.0") - oldps := property.MustSchemaID("aaaaaa~1.0.0/_") + oldps := property.MustSchemaID("aaaaaa~1.0.0/@") olde1ps := property.MustSchemaID("aaaaaa~1.0.0/a") olde2ps := property.MustSchemaID("aaaaaa~1.0.0/b") olde3ps := property.MustSchemaID("aaaaaa~1.0.0/c") diff --git a/pkg/plugin/manifest/parser.go b/pkg/plugin/manifest/parser.go index e6729cb8..292d4b7c 100644 --- a/pkg/plugin/manifest/parser.go +++ b/pkg/plugin/manifest/parser.go @@ -16,14 +16,14 @@ var ( ErrSystemManifest = errors.New("cannot build system manifest") ) -func Parse(source io.Reader, scene *plugin.SceneID) (*Manifest, error) { +func Parse(source io.Reader, scene *plugin.SceneID, tl *TranslatedRoot) (*Manifest, error) { root := Root{} if err := yaml.NewDecoder(source).Decode(&root); err != nil { return nil, ErrFailedToParseManifest // return nil, fmt.Errorf("failed to parse plugin manifest: %w", err) } - manifest, err := root.manifest(scene) + manifest, err := root.manifest(scene, tl) if err != nil { return nil, err } @@ -34,14 +34,14 @@ func Parse(source io.Reader, scene *plugin.SceneID) (*Manifest, error) { return manifest, nil } -func ParseSystemFromBytes(source []byte, scene *plugin.SceneID) (*Manifest, error) { +func ParseSystemFromBytes(source []byte, scene *plugin.SceneID, tl *TranslatedRoot) (*Manifest, error) { root := Root{} if err := yaml.Unmarshal(source, &root); err != nil { return nil, ErrFailedToParseManifest // return nil, fmt.Errorf("failed to parse plugin manifest: %w", err) } - manifest, err := root.manifest(scene) + manifest, err := root.manifest(scene, tl) if err != nil { return nil, err } @@ -49,8 +49,8 @@ func ParseSystemFromBytes(source []byte, scene *plugin.SceneID) (*Manifest, erro return manifest, nil } -func MustParseSystemFromBytes(source []byte, scene *plugin.SceneID) *Manifest { - m, err := ParseSystemFromBytes(source, scene) +func MustParseSystemFromBytes(source []byte, scene *plugin.SceneID, tl *TranslatedRoot) *Manifest { + m, err := ParseSystemFromBytes(source, scene, tl) if err != nil { panic(err) } diff --git a/pkg/plugin/manifest/parser_test.go b/pkg/plugin/manifest/parser_test.go index bb8df4fc..30fbde10 100644 --- a/pkg/plugin/manifest/parser_test.go +++ b/pkg/plugin/manifest/parser_test.go @@ -92,7 +92,7 @@ func TestParse(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - m, err := Parse(strings.NewReader(tc.input), nil) + m, err := Parse(strings.NewReader(tc.input), nil, nil) if tc.err == nil { if !assert.NoError(t, err) { return @@ -136,7 +136,7 @@ func TestParseSystemFromBytes(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - m, err := ParseSystemFromBytes([]byte(tc.input), nil) + m, err := ParseSystemFromBytes([]byte(tc.input), nil, nil) if tc.err == nil { if !assert.NoError(t, err) { return @@ -182,12 +182,12 @@ func TestMustParseSystemFromBytes(t *testing.T) { if tc.fails { assert.Panics(t, func() { - _ = MustParseSystemFromBytes([]byte(tc.input), nil) + _ = MustParseSystemFromBytes([]byte(tc.input), nil, nil) }) return } - m := MustParseSystemFromBytes([]byte(tc.input), nil) + m := MustParseSystemFromBytes([]byte(tc.input), nil, nil) assert.Equal(t, m, tc.expected) }) } diff --git a/pkg/plugin/manifest/parser_translation.go b/pkg/plugin/manifest/parser_translation.go index 75a2eac6..0fec6c14 100644 --- a/pkg/plugin/manifest/parser_translation.go +++ b/pkg/plugin/manifest/parser_translation.go @@ -8,8 +8,6 @@ import ( "io" "github.com/goccy/go-yaml" - "github.com/reearth/reearth-backend/pkg/plugin" - "github.com/reearth/reearth-backend/pkg/property" ) var ( @@ -17,150 +15,27 @@ var ( ErrFailedToParseManifestTranslation error = errors.New("failed to parse plugin manifest translation") ) -func ParseTranslation(source io.Reader) (*TranslationRoot, error) { +func ParseTranslation(source io.Reader) (TranslationRoot, error) { root := TranslationRoot{} if err := yaml.NewDecoder(source).Decode(&root); err != nil { - return nil, ErrFailedToParseManifestTranslation - // return nil, fmt.Errorf("failed to parse plugin manifest translation: %w", err) + return root, ErrFailedToParseManifestTranslation } - return &root, nil + return root, nil } -func ParseTranslationFromBytes(source []byte) (*TranslationRoot, error) { +func ParseTranslationFromBytes(source []byte) (TranslationRoot, error) { tr := TranslationRoot{} if err := yaml.Unmarshal(source, &tr); err != nil { - return nil, ErrFailedToParseManifestTranslation - // return nil, fmt.Errorf("failed to parse plugin manifest translation: %w", err) + return tr, ErrFailedToParseManifestTranslation } - return &tr, nil + return tr, nil } -func MustParseTranslationFromBytes(source []byte) *TranslationRoot { +func MustParseTranslationFromBytes(source []byte) TranslationRoot { m, err := ParseTranslationFromBytes(source) if err != nil { panic(err) } return m } - -func MergeManifestTranslation(m *Manifest, tl map[string]*TranslationRoot) *Manifest { - for lang, t := range tl { - if t == nil { - continue - } - - if t.Name != nil { - name := m.Plugin.Name() - if name == nil { - name = map[string]string{} - } - name[lang] = *t.Name - m.Plugin.Rename(name) - } - - if t.Description != nil { - des := m.Plugin.Description() - if des == nil { - des = map[string]string{} - } - des[lang] = *t.Description - m.Plugin.SetDescription(des) - } - - for key, te := range t.Extensions { - ext := m.Plugin.Extension(plugin.ExtensionID(key)) - if ext == nil { - continue - } - - if te.Name != nil { - name := ext.Name() - if name == nil { - name = map[string]string{} - } - name[lang] = *te.Name - ext.Rename(name) - } - - if te.Description != nil { - des := ext.Description() - if des == nil { - des = map[string]string{} - } - des[lang] = *te.Description - ext.SetDescription(des) - } - - var ps *property.Schema - for _, s := range m.ExtensionSchema { - if s.ID() == ext.Schema() { - ps = s - break - } - } - if ps == nil { - continue - } - - for key, tsg := range te.PropertySchema { - psg := ps.Groups().Group(property.SchemaGroupID(key)) - if psg == nil { - continue - } - - if tsg.Title != nil { - t := psg.Title() - if t == nil { - t = map[string]string{} - } - t[lang] = *tsg.Title - psg.SetTitle(t) - } - - // PropertySchemaGroup does not have description for now - // if tsg.Description != nil { - // t := psg.Description() - // t[lang] = *tsg.Description - // psg.SetDescription(t) - // } - - for key, tsf := range tsg.Fields { - psf := psg.Field(property.FieldID(key)) - if psf == nil { - continue - } - - if tsf.Title != nil { - t := psf.Title() - if t == nil { - t = map[string]string{} - } - t[lang] = *tsf.Title - psf.SetTitle(t) - } - - if tsf.Description != nil { - t := psf.Description() - if t == nil { - t = map[string]string{} - } - t[lang] = *tsf.Description - psf.SetDescription(t) - } - - for key, label := range tsf.Choices { - psfc := psf.Choice(key) - if psfc == nil { - continue - } - - psfc.Title[lang] = label - } - } - } - } - } - - return m -} diff --git a/pkg/plugin/manifest/parser_translation_test.go b/pkg/plugin/manifest/parser_translation_test.go index 10da9227..84ff8066 100644 --- a/pkg/plugin/manifest/parser_translation_test.go +++ b/pkg/plugin/manifest/parser_translation_test.go @@ -5,14 +5,12 @@ import ( "strings" "testing" - "github.com/reearth/reearth-backend/pkg/i18n" - "github.com/reearth/reearth-backend/pkg/plugin" "github.com/stretchr/testify/assert" ) //go:embed testdata/translation.yml var translatedManifest string -var expected = &TranslationRoot{ +var expected = TranslationRoot{ Description: sr("test plugin desc"), Extensions: map[string]TranslationExtension{ "test_ext": { @@ -37,14 +35,11 @@ var expected = &TranslationRoot{ Schema: nil, } -//go:embed testdata/translation_merge.yml -var mergeManifest string - func TestParseTranslation(t *testing.T) { tests := []struct { name string input string - expected *TranslationRoot + expected TranslationRoot err error }{ { @@ -56,7 +51,7 @@ func TestParseTranslation(t *testing.T) { { name: "fail not valid JSON", input: "", - expected: nil, + expected: TranslationRoot{}, err: ErrFailedToParseManifestTranslation, }, } @@ -80,7 +75,7 @@ func TestParseTranslationFromBytes(t *testing.T) { tests := []struct { name string input string - expected *TranslationRoot + expected TranslationRoot err error }{ { @@ -92,7 +87,7 @@ func TestParseTranslationFromBytes(t *testing.T) { { name: "fail not valid YAML", input: "--", - expected: nil, + expected: TranslationRoot{}, err: ErrFailedToParseManifestTranslation, }, } @@ -115,7 +110,7 @@ func TestMustParseTransSystemFromBytes(t *testing.T) { tests := []struct { name string input string - expected *TranslationRoot + expected TranslationRoot fails bool }{ { @@ -127,7 +122,7 @@ func TestMustParseTransSystemFromBytes(t *testing.T) { { name: "fail not valid YAML", input: "--", - expected: nil, + expected: TranslationRoot{}, fails: true, }, } @@ -150,55 +145,6 @@ func TestMustParseTransSystemFromBytes(t *testing.T) { } } -func TestMergeManifestTranslation(t *testing.T) { - tests := []struct { - Name string - Translations map[string]*TranslationRoot - Manifest *Manifest - Expected *struct { - PluginName, PluginDesc, ExtName, PsTitle, FieldTitle, FieldDesc i18n.String - } - }{ - { - Name: "nil translition list", - Translations: nil, - Manifest: nil, - Expected: nil, - }, - { - Name: "nil translition list", - Translations: map[string]*TranslationRoot{"xx": MustParseTranslationFromBytes([]byte(translatedManifest))}, - Manifest: MustParseSystemFromBytes([]byte(mergeManifest), nil), - Expected: &struct{ PluginName, PluginDesc, ExtName, PsTitle, FieldTitle, FieldDesc i18n.String }{ - PluginName: i18n.String{"en": "aaa", "xx": "test plugin name"}, - PluginDesc: i18n.String{"en": "ddd", "xx": "test plugin desc"}, - ExtName: i18n.String{"en": "ttt", "xx": "test ext name"}, - PsTitle: i18n.String{"en": "sss", "xx": "test ps title"}, - FieldTitle: i18n.String{"en": "nnn", "xx": "test field name"}, - FieldDesc: i18n.String{"en": "kkk", "xx": "test field desc"}, - }, - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.Name, func(t *testing.T) { - t.Parallel() - res := MergeManifestTranslation(tc.Manifest, tc.Translations) - if tc.Expected == nil { - assert.Nil(t, res) - return - } - assert.Equal(t, tc.Expected.PluginName, res.Plugin.Name()) - assert.Equal(t, tc.Expected.PluginDesc, res.Plugin.Description()) - assert.Equal(t, tc.Expected.ExtName, res.Plugin.Extension(plugin.ExtensionID("test_ext")).Name()) - assert.Equal(t, tc.Expected.PsTitle, res.ExtensionSchema[0].Groups().Group("test_ps").Title()) - assert.Equal(t, tc.Expected.FieldTitle, res.ExtensionSchema[0].Groups().Group("test_ps").Field("test_field").Title()) - assert.Equal(t, tc.Expected.FieldDesc, res.ExtensionSchema[0].Groups().Group("test_ps").Field("test_field").Description()) - }) - } -} - func sr(s string) *string { return &s } diff --git a/pkg/plugin/manifest/schema_translation.go b/pkg/plugin/manifest/schema_translation.go index 7d512c19..acd606e2 100644 --- a/pkg/plugin/manifest/schema_translation.go +++ b/pkg/plugin/manifest/schema_translation.go @@ -1,5 +1,7 @@ package manifest +import "github.com/reearth/reearth-backend/pkg/i18n" + type TranslationExtension struct { Description *string `json:"description,omitempty"` Name *string `json:"name,omitempty"` @@ -26,3 +28,229 @@ type TranslationRoot struct { Name *string `json:"name,omitempty"` Schema TranslationPropertySchema `json:"schema,omitempty"` } + +type TranslationMap map[string]TranslationRoot + +type TranslatedExtension struct { + Description i18n.String + Name i18n.String + PropertySchema TranslatedPropertySchema +} + +type TranslatedPropertySchema map[string]*TranslatedPropertySchemaGroup + +type TranslatedPropertySchemaField struct { + Choices map[string]i18n.String + Description i18n.String + Title i18n.String +} + +type TranslatedPropertySchemaGroup struct { + Description i18n.String + Fields map[string]*TranslatedPropertySchemaField + Title i18n.String +} + +type TranslatedRoot struct { + Description i18n.String + Extensions map[string]*TranslatedExtension + Name i18n.String + Schema TranslatedPropertySchema +} + +func (tm TranslationMap) Translated() (res TranslatedRoot) { + if len(tm) == 0 { + return TranslatedRoot{} + } + + res.Name = tm.name() + res.Description = tm.description() + res.Schema.setPropertySchema(tm.propertySchemas("")) + + for l, t := range tm { + for eid, e := range t.Extensions { + te := res.getOrCreateExtension(eid) + + if e.Name != nil { + if te.Name == nil { + te.Name = i18n.String{} + } + te.Name[l] = *e.Name + } + + if e.Description != nil { + if te.Description == nil { + te.Description = i18n.String{} + } + te.Description[l] = *e.Description + } + + if len(e.PropertySchema) > 0 { + te.PropertySchema.setPropertySchema(tm.propertySchemas(eid)) + } + } + } + + return res +} + +func (tm TranslationMap) TranslatedRef() *TranslatedRoot { + if len(tm) == 0 { + return nil + } + + t := tm.Translated() + return &t +} +func (t TranslationRoot) propertySchema(eid string) (res TranslationPropertySchema) { + if eid == "" { + return t.Schema + } + for eid2, e := range t.Extensions { + if eid == eid2 { + return e.PropertySchema + } + } + return +} + +func (tm TranslationMap) name() i18n.String { + name := i18n.String{} + for l, t := range tm { + if t.Name == nil { + continue + } + name[l] = *t.Name + } + if len(name) == 0 { + return nil + } + return name +} + +func (tm TranslationMap) description() i18n.String { + desc := i18n.String{} + for l, t := range tm { + if t.Description == nil { + continue + } + desc[l] = *t.Description + } + if len(desc) == 0 { + return nil + } + return desc +} + +func (tm TranslationMap) propertySchemas(eid string) map[string]TranslationPropertySchema { + if len(tm) == 0 { + return nil + } + + res := make(map[string]TranslationPropertySchema) + for l, tl := range tm { + s := tl.propertySchema(eid) + res[l] = s + } + return res +} + +func (t *TranslatedRoot) getOrCreateExtension(eid string) *TranslatedExtension { + if eid == "" { + return nil + } + if t.Extensions == nil { + t.Extensions = map[string]*TranslatedExtension{} + } + if e, ok := t.Extensions[eid]; ok { + return e + } + g := &TranslatedExtension{} + t.Extensions[eid] = g + return g +} + +func (t *TranslatedPropertySchema) getOrCreateGroup(gid string) *TranslatedPropertySchemaGroup { + if gid == "" { + return nil + } + if t == nil || *t == nil { + *t = TranslatedPropertySchema{} + } + if g := (*t)[gid]; g != nil { + return g + } + g := &TranslatedPropertySchemaGroup{} + (*t)[gid] = g + return g +} + +func (t *TranslatedPropertySchemaGroup) getOrCreateField(fid string) *TranslatedPropertySchemaField { + if fid == "" { + return nil + } + if t.Fields == nil { + t.Fields = map[string]*TranslatedPropertySchemaField{} + } + if f := t.Fields[fid]; f != nil { + return f + } + f := &TranslatedPropertySchemaField{} + t.Fields[fid] = f + return f +} + +func (t *TranslatedPropertySchema) setPropertySchema(schemas map[string]TranslationPropertySchema) { + for l, tl := range schemas { + for gid, g := range tl { + if t == nil || *t == nil { + *t = TranslatedPropertySchema{} + } + + tg := t.getOrCreateGroup(gid) + + if g.Title != nil { + if tg.Title == nil { + tg.Title = i18n.String{} + } + tg.Title[l] = *g.Title + } + + if g.Description != nil { + if tg.Description == nil { + tg.Description = i18n.String{} + } + tg.Description[l] = *g.Description + } + + for fid, f := range g.Fields { + tf := tg.getOrCreateField(fid) + if f.Title != nil { + if tf.Title == nil { + tf.Title = i18n.String{} + } + tf.Title[l] = *f.Title + } + + if f.Description != nil { + if tf.Description == nil { + tf.Description = i18n.String{} + } + tf.Description[l] = *f.Description + } + + if len(f.Choices) > 0 { + if tf.Choices == nil { + tf.Choices = map[string]i18n.String{} + } + for cid, c := range f.Choices { + if tf.Choices[cid] == nil { + tf.Choices[cid] = i18n.String{} + } + tf.Choices[cid][l] = c + } + } + } + } + } +} diff --git a/pkg/plugin/manifest/schema_translation_test.go b/pkg/plugin/manifest/schema_translation_test.go new file mode 100644 index 00000000..ba158d2c --- /dev/null +++ b/pkg/plugin/manifest/schema_translation_test.go @@ -0,0 +1,188 @@ +package manifest + +import ( + "testing" + + "github.com/reearth/reearth-backend/pkg/i18n" + "github.com/stretchr/testify/assert" +) + +func TestTranslationMap_Translated(t *testing.T) { + m := TranslationMap{ + "en": TranslationRoot{ + Name: sr("Name"), + Description: sr("desc"), + Extensions: map[string]TranslationExtension{ + "a": { + Name: sr("ext"), + PropertySchema: TranslationPropertySchema{ + "default": { + Fields: map[string]TranslationPropertySchemaField{ + "foo": {Title: sr("foo"), Choices: map[string]string{"A": "AAA", "B": "BBB"}}, + "hoge": {Title: sr("hoge")}, + }, + }, + }, + }, + }, + Schema: TranslationPropertySchema{ + "another": { + Fields: map[string]TranslationPropertySchemaField{ + "foo": {Choices: map[string]string{"A": "AAA"}}, + }, + }, + }, + }, + "ja": TranslationRoot{ + Name: sr("名前"), + Extensions: map[string]TranslationExtension{ + "a": { + Name: sr("extJA"), + Description: sr("DESC!"), + PropertySchema: TranslationPropertySchema{ + "default": { + Fields: map[string]TranslationPropertySchemaField{ + "foo": { + Title: sr("foo!"), + Description: sr("DESC"), + Choices: map[string]string{"B": "BBB!", "C": "CCC!"}, + }, + "bar": {Title: sr("bar!")}, + }, + }, + }, + }, + "b": { + Name: sr("ext2"), + PropertySchema: TranslationPropertySchema{}, + }, + }, + Schema: TranslationPropertySchema{ + "default": { + Fields: map[string]TranslationPropertySchemaField{ + "a": {Title: sr("あ")}, + }, + }, + }, + }, + "zh-CN": TranslationRoot{ + Name: sr("命名"), + Schema: TranslationPropertySchema{ + "another": { + Description: sr("描述"), + }, + }, + }, + } + + expected := TranslatedRoot{ + Name: i18n.String{"en": "Name", "ja": "名前", "zh-CN": "命名"}, + Description: i18n.String{"en": "desc"}, + Extensions: map[string]*TranslatedExtension{ + "a": { + Name: i18n.String{"en": "ext", "ja": "extJA"}, + Description: i18n.String{"ja": "DESC!"}, + PropertySchema: TranslatedPropertySchema{ + "default": &TranslatedPropertySchemaGroup{ + Fields: map[string]*TranslatedPropertySchemaField{ + "foo": { + Title: i18n.String{"en": "foo", "ja": "foo!"}, + Description: i18n.String{"ja": "DESC"}, + Choices: map[string]i18n.String{ + "A": {"en": "AAA"}, + "B": {"en": "BBB", "ja": "BBB!"}, + "C": {"ja": "CCC!"}, + }, + }, + "hoge": { + Title: i18n.String{"en": "hoge"}, + }, + "bar": { + Title: i18n.String{"ja": "bar!"}, + }, + }, + }, + }, + }, + "b": { + Name: i18n.String{"ja": "ext2"}, + }, + }, + Schema: TranslatedPropertySchema{ + "default": { + Title: nil, + Description: nil, + Fields: map[string]*TranslatedPropertySchemaField{ + "a": {Title: i18n.String{"ja": "あ"}}, + }, + }, + "another": { + Title: nil, + Description: i18n.String{"zh-CN": "描述"}, + Fields: map[string]*TranslatedPropertySchemaField{ + "foo": {Choices: map[string]i18n.String{"A": {"en": "AAA"}}}, + }, + }, + }, + } + + assert.Equal(t, expected, m.Translated()) + assert.Equal(t, TranslatedRoot{}, TranslationMap{}.Translated()) + assert.Equal(t, TranslatedRoot{}, TranslationMap(nil).Translated()) +} + +func TestTranslatedPropertySchema_getOrCreateGroup(t *testing.T) { + target := TranslatedPropertySchema{} + expected := TranslatedPropertySchema{ + "a": {Title: i18n.String{"ja": "A"}}, + } + + group := target.getOrCreateGroup("a") + assert.Equal(t, &TranslatedPropertySchemaGroup{}, group) + + group.Title = i18n.String{"ja": "A"} + assert.Equal(t, expected, target) +} + +func TestTranslatedPropertySchema_getOrCreateField(t *testing.T) { + target := TranslatedPropertySchemaGroup{} + expected := TranslatedPropertySchemaGroup{ + Fields: map[string]*TranslatedPropertySchemaField{ + "a": {Title: i18n.String{"ja": "A"}}, + }, + } + + field := target.getOrCreateField("a") + assert.Equal(t, &TranslatedPropertySchemaField{}, field) + + field.Title = i18n.String{"ja": "A"} + assert.Equal(t, expected, target) +} + +func TestTranslatedPropertySchema_setPropertySchema(t *testing.T) { + target := TranslatedPropertySchema{ + "a": nil, + "b": {}, + } + expected := TranslatedPropertySchema{ + "a": { + Title: i18n.String{"ja": "A"}, + Fields: map[string]*TranslatedPropertySchemaField{ + "f": {Title: i18n.String{"en": "F"}}, + }}, + "b": {Title: i18n.String{"en": "B"}}, + } + + target.setPropertySchema(map[string]TranslationPropertySchema{ + "en": { + "a": { + Fields: map[string]TranslationPropertySchemaField{ + "f": {Title: sr("F")}, + }, + }, + "b": {Title: sr("B")}, + }, + "ja": {"a": {Title: sr("A")}}, + }) + assert.Equal(t, expected, target) +} diff --git a/pkg/plugin/manifest/testdata/translation_merge.yml b/pkg/plugin/manifest/testdata/translation_merge.yml deleted file mode 100644 index 6f23f4a0..00000000 --- a/pkg/plugin/manifest/testdata/translation_merge.yml +++ /dev/null @@ -1,34 +0,0 @@ -{ - "id": "xxx", - "name": "aaa", - "version": "1.1.1", - "description": "ddd", - "extensions": - [ - { - "id": "test_ext", - "name": "ttt", - "visualizer": "cesium", - "type": "primitive", - "schema": - { - "groups": - [ - { - "id": "test_ps", - "title": "sss", - "fields": - [ - { - "id": "test_field", - "title": "nnn", - "type": "string", - "description": "kkk", - }, - ], - }, - ], - }, - }, - ], -} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 0a12d09d..37b2d5be 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -34,7 +34,7 @@ func (p *Plugin) Name() i18n.String { if p == nil { return nil } - return p.name.Copy() + return p.name.Clone() } func (p *Plugin) Author() string { @@ -48,7 +48,7 @@ func (p *Plugin) Description() i18n.String { if p == nil { return nil } - return p.description.Copy() + return p.description.Clone() } func (p *Plugin) RepositoryURL() string { @@ -127,26 +127,12 @@ func (p *Plugin) Clone() *Plugin { return &Plugin{ id: p.id.Clone(), - name: p.name.Copy(), + name: p.name.Clone(), author: p.author, - description: p.description.Copy(), + description: p.description.Clone(), repositoryURL: p.repositoryURL, extensions: extensions, extensionOrder: extensionOrder, schema: p.schema.CopyRef(), } } - -func (p *Plugin) Rename(name i18n.String) { - if p == nil { - return - } - p.name = name.Copy() -} - -func (p *Plugin) SetDescription(des i18n.String) { - if p == nil { - return - } - p.description = des.Copy() -} diff --git a/pkg/plugin/plugin_test.go b/pkg/plugin/plugin_test.go index 1917a58b..9b01cda4 100644 --- a/pkg/plugin/plugin_test.go +++ b/pkg/plugin/plugin_test.go @@ -79,18 +79,6 @@ func TestPlugin_PropertySchemas(t *testing.T) { } } -func TestPlugin_Rename(t *testing.T) { - p := New().Name(i18n.StringFrom("x")).MustBuild() - p.Rename(i18n.StringFrom("z")) - assert.Equal(t, i18n.StringFrom("z"), p.Name()) -} - -func TestPlugin_SetDescription(t *testing.T) { - p := New().MustBuild() - p.SetDescription(i18n.StringFrom("xxx")) - assert.Equal(t, i18n.StringFrom("xxx"), p.Description()) -} - func TestPlugin_Author(t *testing.T) { p := New().Author("xx").MustBuild() assert.Equal(t, "xx", p.Author()) diff --git a/pkg/plugin/pluginpack/package.go b/pkg/plugin/pluginpack/package.go index c7e9344c..cc754454 100644 --- a/pkg/plugin/pluginpack/package.go +++ b/pkg/plugin/pluginpack/package.go @@ -6,6 +6,7 @@ import ( "io" "path" "path/filepath" + "regexp" "github.com/reearth/reearth-backend/pkg/file" "github.com/reearth/reearth-backend/pkg/plugin" @@ -15,6 +16,8 @@ import ( const manfiestFilePath = "reearth.yml" +var translationFileNameRegexp = regexp.MustCompile(`reearth_([a-zA-Z]+(?:-[a-zA-Z]+)?).yml`) + type Package struct { Manifest *manifest.Manifest Files file.Iterator @@ -32,6 +35,7 @@ func PackageFromZip(r io.Reader, scene *plugin.SceneID, sizeLimit int64) (*Packa } basePath := file.ZipBasePath(zr) + f, err := zr.Open(path.Join(basePath, manfiestFilePath)) if err != nil { return nil, rerror.From("manifest open error", err) @@ -40,7 +44,12 @@ func PackageFromZip(r io.Reader, scene *plugin.SceneID, sizeLimit int64) (*Packa _ = f.Close() }() - m, err := manifest.Parse(f, scene) + translations, err := readTranslation(zr, basePath) + if err != nil { + return nil, err + } + + m, err := manifest.Parse(f, scene, translations.TranslatedRef()) if err != nil { return nil, rerror.From("invalid manifest", err) } @@ -56,3 +65,31 @@ func iterator(a file.Iterator, prefix string) file.Iterator { return p == manfiestFilePath || filepath.Ext(p) != ".js" }) } + +func readTranslation(fs *zip.Reader, base string) (manifest.TranslationMap, error) { + translationMap := manifest.TranslationMap{} + for _, f := range fs.File { + if filepath.Dir(f.Name) != base { + continue + } + + lang := translationFileNameRegexp.FindStringSubmatch(filepath.Base(f.Name)) + if len(lang) == 0 { + continue + } + langfile, err := f.Open() + if err != nil { + return nil, rerror.ErrInternalBy(err) + } + defer func() { + _ = langfile.Close() + }() + t, err := manifest.ParseTranslation(langfile) + if err != nil { + return nil, err + } + translationMap[lang[1]] = t + } + + return translationMap, nil +} diff --git a/pkg/plugin/pluginpack/package_test.go b/pkg/plugin/pluginpack/package_test.go index ecd2a807..461107bf 100644 --- a/pkg/plugin/pluginpack/package_test.go +++ b/pkg/plugin/pluginpack/package_test.go @@ -12,22 +12,22 @@ import ( ) func TestPackageFromZip(t *testing.T) { + expected := &manifest.Manifest{ + Plugin: plugin.New(). + ID(plugin.MustID("testplugin~1.0.1")). + Name(i18n.String{"en": "testplugin", "ja": "テストプラグイン", "zh-CN": "测试插件"}). + MustBuild(), + } + f, err := os.Open("testdata/test.zip") assert.NoError(t, err) defer func() { _ = f.Close() }() - expected := plugin.New(). - ID(plugin.MustID("testplugin~1.0.1")). - Name(i18n.StringFrom("testplugin")). - MustBuild() - - p, err := PackageFromZip(f, nil, 1000) + p, err := PackageFromZip(f, nil, 10000) assert.NoError(t, err) - assert.Equal(t, &manifest.Manifest{ - Plugin: expected, - }, p.Manifest) + assert.Equal(t, expected, p.Manifest) var files []string for { diff --git a/pkg/plugin/pluginpack/testdata/test.zip b/pkg/plugin/pluginpack/testdata/test.zip index b13b0aba34f9750cd98e656a318482707130cddb..0d371acbe428c4d8d123d08cbb4d70400e987e89 100644 GIT binary patch literal 1804 zcmWIWW@h1H0D;&!o*`fclwf6$VJJy0F3}GS;bdUGG=n+m3J{l8a5FHnd}U-{U=aZ- z3;=2e(Hsoxy={YUmg^Z;f}|uE82Dh?^<0AEgG=&@Qo-hAZ4OSnG!?^~w=>TBH5*7A z*l!flVOV5Pcsy>;+XE%DDhw1Y`0x1`RLw|hc97u=GM>?`)OP)X`6tdA+3O!O1GYCW z*M0X}_3V0G-KX43>KPc)`2VUkFfvF@2`ib=)%H@{>!{DY!_undaVmQXf9C$$+rjF* z&ssEyL!oiYuYY@XYxo3Ru1WXujg5@GToQZP{7d$7eK)^X%MKqYW0#GzD!#n0{b#Y_ zqV;=T$F>|?k)0#kZBcsILc+(Yz0M%7s8sUUK8c%262Co{7bO}6TwjrXb6=)e#pREn7@qV}`d1PWoe!{1NLM)Ct z+7_%?uRquFG%1J)SlSnCY1Q*G-m`AORWEtwvlqp7X`J0Kw<+wR)Gdc%kksQPM?ZWx zSdh}DE|Rvu^47256D%7|u6Rin{k5N2`%j(Yk^JE++b(ZQ{AMq>^V+AvSG8pg@1+^n z1aDj$)Af34Ro}J?u9>$u1dii@>7P)Wo8ajQFfX zy~^Aia4h}+#vllzrG{z+rM$%4R3$3~rN_-}kDGQrZti&8JpFO=%EwI`9ycv{+`L&y zp_U7jXdre*6;2EWS_KDOFgpf~A@pjnPUEC4eP8V>M4z~QphVF9On0yhN(XnE@DX`ayZ zJ%1+nQec3Vx31Q?Gv}E-C03$r^j zFD11?FRK{tkGq)uNY2kI&d*8J%gImIP|8Tn$;nqJ&o9bJQPQ+V3i51VHeZ@6ogfV~ z9)!hUR=}dUDnr-V5AMmAz&sDaXu*#Z)6ceczgR!@S^uObySG7Nmyt=18CRJCEE&K+ zfZ+fraA743E2M-$vn9YAWE8IQ1!fcj!;(gOm{CZj4bV(bX@k{FTuB~cCa?$tn#sTf zbsj8aU>N{tFqXs*aV)NM4Rfpj!&}D^n8DCg4m1yI3P*$^C|#hYab$f->yCBV=NVgd00kS+UJ literal 789 zcmWIWW@Zs#-~d8N&Hq6RP#^=OIT#ceGV@YWEA+C8Lqm8O*n?tN5}Eb{B{BkWX$3a} zBg>P7;GM|?N zssx!F0Jrl*olP)M01O;~Bsb8`qSVyHqLK`~%G?~VwUR*VU>MEX)nNgrd;&KG1!#Hd z>S>@=2&T_+p6Gl_Ey4QzWXBYmI=Gfh>eL z1!O<6_d$GCpi@dxi%az3eh2v