diff --git a/perxis-proto b/perxis-proto index 7a5f5018db5d7f84435e1f98a0e94cac9ddaa9ba..63073e8bf4bf41368a71e7e2e207625c4c67838d 160000 --- a/perxis-proto +++ b/perxis-proto @@ -1 +1 @@ -Subproject commit 7a5f5018db5d7f84435e1f98a0e94cac9ddaa9ba +Subproject commit 63073e8bf4bf41368a71e7e2e207625c4c67838d diff --git a/pkg/items/item.go b/pkg/items/item.go index 96c476cbdd15c49f47255929fbab577eb2605b6f..52cc0e36d4fe38892c7c8bba0f080803236a339a 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -3,13 +3,14 @@ package items import ( "context" "fmt" - "reflect" "time" "git.perx.ru/perxis/perxis-go/pkg/data" "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/locales" "git.perx.ru/perxis/perxis-go/pkg/schema" "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "git.perx.ru/perxis/perxis-go/pkg/schema/localizer" pb "git.perx.ru/perxis/perxis-go/proto/items" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -161,30 +162,28 @@ func (i *Item) ToMap() map[string]interface{} { } } -func (i *Item) SetData(locale string, data map[string]interface{}) { - if locale != "" { - if i.Translations == nil { - i.Translations = make(map[string]map[string]interface{}) - } - i.Translations[locale] = data +func (i *Item) SetData(data map[string]interface{}, localizer *localizer.Localizer) (err error) { + if localizer != nil && localizer.LocaleID != locales.DefaultID { + i.Translations[localizer.LocaleID], err = localizer.ExtractTranslation(i.Data, i.Translations) return } + i.Data = data + return } -func (i *Item) GetData(locale string) map[string]interface{} { - if locale != "" && i.Translations != nil { - translation, _ := i.Translations[locale] - return MergeData(i.Data, translation) +func (i *Item) GetData(localizer *localizer.Localizer) (map[string]interface{}, error) { + if localizer != nil { + return localizer.Localize(i.Data, i.Translations) } - return i.Data + return i.Data, nil } func (i Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) { if i.Data != nil { dt, err := schema.Encode(nil, s, i.Data) if err != nil { - //return errors.WithField(err, "data") + // return errors.WithField(err, "data") return nil, err } i.Data = dt.(map[string]interface{}) @@ -193,7 +192,7 @@ func (i Item) Encode(ctx context.Context, s *schema.Schema) (*Item, error) { for l, v := range i.Translations { dt, err := schema.Encode(nil, s, v) if err != nil { - //return errors.WithField(err, fmt.Sprintf("translations.%s", l)) + // return errors.WithField(err, fmt.Sprintf("translations.%s", l)) return nil, err } i.Translations[l] = dt.(map[string]interface{}) @@ -208,47 +207,16 @@ func (i Item) Decode(ctx context.Context, s *schema.Schema) (res *Item, err erro i.Data, err = s.Decode(ctx, i.Data) if err != nil { return nil, err - //return errors.WithField(err, "data") + // return errors.WithField(err, "data") } } return &i, nil } -// MergeData дополняет отсутствующие данные РёР· оригинальных данных -func MergeData(data ...map[string]interface{}) map[string]interface{} { - merge := make(map[string]interface{}) - for _, d := range data { - for k, v := range d { - merge[k] = v - } - } - return merge -} - -// ClearData убирает данные которые РЅРµ изменились РїРѕ сравнению СЃ оригинальными данными -func ClearData(data ...map[string]interface{}) map[string]interface{} { - var clear map[string]interface{} - - for _, d := range data { - if clear == nil { - clear = d - continue - } - - for k, v := range d { - if reflect.DeepEqual(clear[k], v) { - delete(clear, k) - } - } - } - - return clear -} - type ProcessDataFunc func(ctx context.Context, sch *schema.Schema, data map[string]interface{}) (map[string]interface{}, error) -func (i Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDataFunc, locales ...string) (*Item, error) { +func (i Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDataFunc, locales ...*locales.Locale) (*Item, error) { if i.Data != nil { dt, err := fn(ctx, sch, i.Data) if err != nil { @@ -259,14 +227,16 @@ func (i Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDat tr := make(map[string]map[string]interface{}) for _, l := range locales { - - data := i.GetData(l) + data, err := i.GetData(localizer.NewLocalizer(sch, locales, l.ID)) + if err != nil { + return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID)) + } dt, err := fn(ctx, sch, data) if err != nil { - return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l)) + return nil, errors.WithField(err, fmt.Sprintf("translations.%s", l.ID)) } - tr[l] = dt + tr[l.ID] = dt } @@ -280,15 +250,12 @@ func (i Item) ProcessData(ctx context.Context, sch *schema.Schema, fn ProcessDat // IsSystemField возвращает являться ли поле системным func IsSystemField(field string) bool { - if data.Contains(field, SystemFields) { - return true - } - return false + return data.Contains(field, SystemFields) } // SetSystemField устанавливает значение системного поля func (i *Item) SetSystemField(field string, value interface{}) error { - ok := true + var ok bool switch field { case "id": i.ID, ok = value.(string) diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go index dfcc16ee1c7b40441b5df54acc48a12b276669ca..47e7f02be2014b3c94e4a7c0d29d4ccab7dbca63 100644 --- a/pkg/items/item_test.go +++ b/pkg/items/item_test.go @@ -13,14 +13,14 @@ import ( func TestItem_Set(t *testing.T) { item := &Item{} - item.Set("id", "id") + _ = item.Set("id", "id") assert.Equal(t, "id", item.ID) now := time.Now() - item.Set("created_at", now) + _ = item.Set("created_at", now) assert.Equal(t, now, item.CreatedAt) - item.Set("a.b.c", 101) + _ = item.Set("a.b.c", 101) assert.Equal(t, map[string]any{"a": map[string]any{"b": map[string]any{"c": 101}}}, item.Data) } diff --git a/pkg/schema/field/field.go b/pkg/schema/field/field.go index 45ddb40d128ab3f87ff7c41e3cedf75009ba3f98..32b17574e071e8d72eae3eba789c2bb2c6177faa 100644 --- a/pkg/schema/field/field.go +++ b/pkg/schema/field/field.go @@ -55,7 +55,7 @@ type Field struct { Translations []Translation `json:"translations,omitempty"` // Переводы данных РЅР° разных языках UI *UI `json:"ui,omitempty"` // Опции пользовательского интерфейса Includes []Include `json:"includes,omitempty"` // РРјРїРѕСЂС‚ схем - SingleLocale bool `json:"singleLocale,omitempty"` // Без перевода + SingleLocale bool `json:"single_locale,omitempty"` // Без перевода Indexed bool `json:"indexed,omitempty"` // Построить индекс для поля Unique bool `json:"unique,omitempty"` // Значение поля должны быть уникальными TextSearch bool `json:"text_search,omitempty"` // Значение поля доступны для полнотекстового РїРѕРёСЃРєР° @@ -335,11 +335,11 @@ func (f *Field) GetFields(filterFunc FilterFunc, pathPrefix ...string) (res []Pa res = append(res, getFieldsArray(path, params, filterFunc)...) } - //if len(pathPrefix) > 0 { + // if len(pathPrefix) > 0 { // for _, r := range res { // r.Path = strings.Join([]string{pathPrefix[0], r.Path}, FieldSeparator) // } - //} + // } return res } diff --git a/pkg/schema/localizer/localizer.go b/pkg/schema/localizer/localizer.go new file mode 100644 index 0000000000000000000000000000000000000000..a818f6e922993330ea0c4bca396b5a51c49bec37 --- /dev/null +++ b/pkg/schema/localizer/localizer.go @@ -0,0 +1,192 @@ +package localizer + +import ( + "context" + "reflect" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/locales" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "git.perx.ru/perxis/perxis-go/pkg/schema/walk" +) + +type Localizer struct { + Schema *schema.Schema + Locales map[string]*locales.Locale + LocaleID string +} + +// NewLocalizer создает экземпляр локализатора. Требуется указать "загруженную" схему +func NewLocalizer(s *schema.Schema, locs []*locales.Locale, localeID string) *Localizer { + if localeID == "" { + localeID = locales.DefaultID + } + + loc := &Localizer{ + Schema: s, + Locales: make(map[string]*locales.Locale, len(locs)), + LocaleID: localeID, + } + + for _, l := range locs { + loc.Locales[l.ID] = l + } + + return loc +} + +// Localize Получить полные локализованные данные для локали `localeID`. Входные параметры: +// - `data map[string]interface{}` - данные РѕСЃРЅРѕРІРЅРѕР№ локали +// - `translations map[string]map[string]interface{}` переводы +// +// РџСЂРё отсутствии каких-либо полей РІ переводе РЅР° `localeID` данные берутся сначала РёР· fallback-локали, +// если перевод отсутствует то РёР· `data` +func (l *Localizer) Localize(data map[string]interface{}, translations map[string]map[string]interface{}) (localized map[string]interface{}, err error) { + if l.LocaleID == locales.DefaultID { + return data, nil + } + + target, fallback, err := l.getTargetAndFallBackLocales() + if err != nil { + return nil, err + } + + // localize fallback -> target + fallbackData := data + var exist bool + if !fallback.IsDefault() { + if fd, exist := translations[fallback.ID]; exist { + // localize default -> fallback + fallbackData, err = l.localize(fd, data) + if err != nil { + return nil, err + } + } + } + + if localized, exist = translations[target.ID]; !exist { + localized = make(map[string]interface{}) + } + + return l.localize(localized, fallbackData) +} + +// ExtractTranslation Получить "просеянные" данные для локали localeID: РІСЃРµ поля, значения которых совпадают +// СЃ переводом РЅР° fallback-локаль или основными данными, удаляются РёР· перевода +func (l *Localizer) ExtractTranslation(data map[string]interface{}, translations map[string]map[string]interface{}) (translation map[string]interface{}, err error) { + if l.LocaleID == locales.DefaultID { + return data, nil + } + + target, fallback, err := l.getTargetAndFallBackLocales() + if err != nil { + return nil, err + } + + fallbackData := data + var exist bool + if !fallback.IsDefault() { + if fallbackData, exist = translations[fallback.ID]; !exist { + fallbackData = data + } + } + + // extract translation fallback -> target + if translation, exist = translations[target.ID]; !exist { + return make(map[string]interface{}), nil + } + + return l.extractTranslation(translation, fallbackData) +} + +func (l *Localizer) getTargetAndFallBackLocales() (target, fallback *locales.Locale, err error) { + if target = l.Locales[l.LocaleID]; target == nil || target.Disabled { + return nil, nil, errors.New("target locale not found or disabled") + } + + if fallback = l.Locales[target.Fallback]; fallback == nil || fallback.Disabled { + fallback = l.Locales[locales.DefaultID] + } + return +} + +func (l *Localizer) localize(target, fallback map[string]interface{}) (map[string]interface{}, error) { + if target == nil && fallback == nil { + return nil, nil + } + + single := l.Schema.GetFields(func(f *field.Field, p string) bool { + return f.SingleLocale + }) + + cfg := &walk.WalkConfig{Fields: make(map[string]walk.FieldConfig, len(single))} + for _, sn := range single { + cfg.Fields[sn.Path] = walk.FieldConfig{Fn: walk.KeepSrc} + } + + w := walk.NewWalker(l.Schema, cfg) + w.DefaultFn = localize + + res, _, err := w.DataWalk(context.Background(), target, fallback) + if err != nil { + return nil, err + } + + if res != nil { + return res.(map[string]interface{}), err + } + + return nil, nil +} + +func (l *Localizer) extractTranslation(target, fallback map[string]interface{}) (map[string]interface{}, error) { + if target == nil && fallback == nil { + return nil, nil + } + + single := l.Schema.GetFields(func(f *field.Field, p string) bool { + return f.SingleLocale + }) + + cfg := &walk.WalkConfig{Fields: make(map[string]walk.FieldConfig, len(single))} + for _, sn := range single { + cfg.Fields[sn.Path] = walk.FieldConfig{Fn: walk.RemoveValue} + } + + w := walk.NewWalker(l.Schema, cfg) + w.DefaultFn = extractTranslation + + res, _, err := w.DataWalk(context.Background(), target, fallback) + if err != nil { + return nil, err + } + + if res == nil { + return map[string]interface{}{}, nil + } + + return res.(map[string]interface{}), nil +} + +func localize(c *walk.WalkContext) (err error) { + if c.Dst != nil { + return + } + + c.Dst = c.Src + c.Changed = true + return +} + +func extractTranslation(c *walk.WalkContext) (err error) { + if c.Dst == nil { + return + } + + if reflect.DeepEqual(c.Src, c.Dst) { + c.Dst = nil + c.Changed = true + } + return +} diff --git a/pkg/schema/localizer/localizer_test.go b/pkg/schema/localizer/localizer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6f5fbdd687bb5d613f18fb039340a9489f5aaf9c --- /dev/null +++ b/pkg/schema/localizer/localizer_test.go @@ -0,0 +1,612 @@ +package localizer + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLocalizer_localize(t *testing.T) { + + s := schema.New( + "a", field.String(), + "b", field.Number(field.NumberFormatInt), + "c", field.String().SetSingleLocale(true), + "obj1", field.Object( + "a", field.String(), + "b", field.Number(field.NumberFormatInt), + "c", field.Number(field.NumberFormatInt).SetSingleLocale(true), + "obj2", field.Object( + "a", field.Number(field.NumberFormatInt), + "b", field.String(), + "c", field.String().SetSingleLocale(true), + ), + ), + "obj3", field.Object( + "a", field.String(), + ).SetSingleLocale(true), + "slice", field.Array(field.String()), + "arr", field.Array(field.Object( + "num", field.Number(field.NumberFormatInt), + )), + ) + + tests := []struct { + name string + fallback map[string]interface{} + target map[string]interface{} + want map[string]interface{} + wantErr bool + }{ + { + name: "Nil", + fallback: nil, + target: nil, + want: nil, + }, + { + name: "Empty", + fallback: map[string]interface{}{}, + target: map[string]interface{}{}, + want: map[string]interface{}{}, + }, + { + name: "Target Empty", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{}, + want: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + }, + { + name: "Fallback Empty", + fallback: map[string]interface{}{}, + target: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + want: map[string]interface{}{ + "a": "en", + "b": 1, + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + }, + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + }, + { + name: "Equal", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + want: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + }, + { + name: "Target no single", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{ + "a": "en", + "b": 1, + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + }, + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + want: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + }, + { + name: "Success", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{ + "a": "ru", + "b": 11, + "obj1": map[string]interface{}{ + "a": "obj1_ru", + "b": 22, + "obj2": map[string]interface{}{ + "a": 33, + "b": "obj1_obj2_ru", + }, + }, + "slice": []interface{}{"ru_s3"}, + "arr": []interface{}{ + map[string]interface{}{"num": 1}, + map[string]interface{}{"num": 2}, + }, + }, + want: map[string]interface{}{ + "a": "ru", + "b": 11, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_ru", + "b": 22, + "c": 20, + "obj2": map[string]interface{}{ + "a": 33, + "b": "obj1_obj2_ru", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"ru_s3"}, + "arr": []interface{}{ + map[string]interface{}{"num": 1}, + map[string]interface{}{"num": 2}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &Localizer{ + Schema: s, + } + got, err := l.localize(tt.target, tt.fallback) + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + return + } + require.Error(t, err) + }) + } +} + +func TestLocalizer_deLocalize(t *testing.T) { + + s := schema.New( + "a", field.String(), + "b", field.Number(field.NumberFormatInt), + "c", field.String().SetSingleLocale(true), + "obj1", field.Object( + "a", field.String(), + "b", field.Number(field.NumberFormatInt), + "c", field.Number(field.NumberFormatInt).SetSingleLocale(true), + "obj2", field.Object( + "a", field.Number(field.NumberFormatInt), + "b", field.String(), + "c", field.String().SetSingleLocale(true), + ), + ), + "obj3", field.Object( + "a", field.String(), + ).SetSingleLocale(true), + "slice", field.Array(field.String()), + "arr", field.Array(field.Object( + "num", field.Number(field.NumberFormatInt), + )), + ) + + tests := []struct { + name string + fallback map[string]interface{} + target map[string]interface{} + want map[string]interface{} + wantErr bool + }{ + { + name: "Nil", + fallback: nil, + target: nil, + want: nil, + }, + { + name: "Empty", + fallback: map[string]interface{}{}, + target: map[string]interface{}{}, + want: map[string]interface{}{}, + }, + { + name: "Target Empty", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{}, + want: map[string]interface{}{}, + }, + { + name: "Fallback Empty", + fallback: map[string]interface{}{}, + target: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + want: map[string]interface{}{ + "a": "en", + "b": 1, + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + }, + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + }, + { + name: "Equal", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + want: map[string]interface{}{}, + }, + { + name: "Success", + fallback: map[string]interface{}{ + "a": "en", + "b": 1, + "c": "single", + "obj1": map[string]interface{}{ + "a": "obj1_en", + "b": 2, + "c": 20, + "obj2": map[string]interface{}{ + "a": 3, + "b": "obj1_obj2_en", + "c": "obj1_obj2_single", + }, + }, + "obj3": map[string]interface{}{ + "a": "obj3_en", + }, + "slice": []interface{}{"en_s1", "en_s2"}, + "arr": []interface{}{ + map[string]interface{}{"num": 11}, + map[string]interface{}{"num": 22}, + }, + }, + target: map[string]interface{}{ + "a": "ru", + "b": 11, + "obj1": map[string]interface{}{ + "a": "obj1_ru", + "b": 22, + "obj2": map[string]interface{}{ + "a": 33, + "b": "obj1_obj2_ru", + }, + }, + "slice": []interface{}{"ru_s3"}, + "arr": []interface{}{ + map[string]interface{}{"num": 1}, + map[string]interface{}{"num": 2}, + }, + }, + want: map[string]interface{}{ + "a": "ru", + "b": 11, + "obj1": map[string]interface{}{ + "a": "obj1_ru", + "b": 22, + "obj2": map[string]interface{}{ + "a": 33, + "b": "obj1_obj2_ru", + }, + }, + "slice": []interface{}{"ru_s3"}, + "arr": []interface{}{ + map[string]interface{}{"num": 1}, + map[string]interface{}{"num": 2}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := &Localizer{ + Schema: s, + } + got, err := l.extractTranslation(tt.target, tt.fallback) + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, tt.want, got) + return + } + require.Error(t, err) + }) + } +} diff --git a/pkg/schema/walk/fn.go b/pkg/schema/walk/fn.go index 9b1d4f22316cf37cae1af824d98f611b7dc0bcf7..a4157783278318096cd5c3a9ee8a67ab388223a8 100644 --- a/pkg/schema/walk/fn.go +++ b/pkg/schema/walk/fn.go @@ -14,3 +14,13 @@ func KeepSrc(c *WalkContext) (err error) { c.Changed = true return } + +func RemoveValue(c *WalkContext) (err error) { + if c.Dst == nil { + return + } + + c.Dst = nil + c.Changed = true + return +}