diff --git a/compose/service/locale.go b/compose/service/locale.go index e95976715..60a0c6364 100644 --- a/compose/service/locale.go +++ b/compose/service/locale.go @@ -14,7 +14,8 @@ import ( func (svc resourceTranslationsManager) moduleExtended(ctx context.Context, res *types.Module) (out locale.ResourceTranslationSet, err error) { var ( - k types.LocaleKey + k types.LocaleKey + set locale.ResourceTranslationSet ) for _, tag := range svc.locale.Tags() { @@ -57,19 +58,26 @@ func (svc resourceTranslationsManager) moduleExtended(ctx context.Context, res * Msg: svc.locale.TResourceFor(tag, f.ResourceTranslation(), k.Path), }) - // Extra field bits - converted, err := svc.moduleFieldValidatorErrorHandler(ctx, tag, f, k.Path) + // Expressions + set, err = svc.moduleFieldExpressionsHandler(ctx, tag, f) if err != nil { return nil, err } - out = append(out, converted...) + out = append(out, set...) + + // Extra field bits + set, err = svc.moduleFieldOptionsHandler(ctx, tag, f) + if err != nil { + return nil, err + } + out = append(out, set...) } } return out, nil } -func (svc resourceTranslationsManager) moduleFieldValidatorErrorHandler(ctx context.Context, tag language.Tag, f *types.ModuleField, k string) (locale.ResourceTranslationSet, error) { +func (svc resourceTranslationsManager) moduleFieldExpressionsHandler(ctx context.Context, tag language.Tag, f *types.ModuleField) (locale.ResourceTranslationSet, error) { out := make(locale.ResourceTranslationSet, 0, 10) for i, v := range f.Expressions.Validators { @@ -77,7 +85,8 @@ func (svc resourceTranslationsManager) moduleFieldValidatorErrorHandler(ctx cont rpl := strings.NewReplacer( "{{validatorID}}", strconv.FormatUint(vContentID, 10), ) - tKey := rpl.Replace(k) + + tKey := rpl.Replace(types.LocaleKeyModuleFieldValidatorError.Path) out = append(out, &locale.ResourceTranslation{ Resource: f.ResourceTranslation(), @@ -90,6 +99,48 @@ func (svc resourceTranslationsManager) moduleFieldValidatorErrorHandler(ctx cont return out, nil } +func (svc resourceTranslationsManager) moduleFieldOptionsHandler(ctx context.Context, tag language.Tag, f *types.ModuleField) (locale.ResourceTranslationSet, error) { + out := make(locale.ResourceTranslationSet, 0, 10) + + optsUnknown, has := f.Options["options"] + if !has { + return nil, nil + } + + optsSlice, is := optsUnknown.([]interface{}) + if !is { + return nil, nil + } + + for _, optUnknown := range optsSlice { + var value string + + // what is this we're dealing with? + // slice of strings (values) or map (value+text) + switch opt := optUnknown.(type) { + case string: + value = opt + + case map[string]interface{}: + value, is = opt["value"].(string) + if !is { + continue + } + } + + trKey := strings.NewReplacer("{{value}}", value).Replace(types.LocaleKeyModuleFieldOptionsOptionTexts.Path) + + out = append(out, &locale.ResourceTranslation{ + Resource: f.ResourceTranslation(), + Lang: tag.String(), + Key: trKey, + Msg: svc.locale.TResourceFor(tag, f.ResourceTranslation(), trKey), + }) + } + + return out, nil +} + func (svc resourceTranslationsManager) pageExtended(ctx context.Context, res *types.Page) (out locale.ResourceTranslationSet, err error) { var ( k types.LocaleKey @@ -121,7 +172,7 @@ func (svc resourceTranslationsManager) pageExtended(ctx context.Context, res *ty switch block.Kind { case "Automation": - aux, err := svc.pageExtendedAutomatinBlock(tag, res, block, pbContentID, k) + aux, err := svc.pageExtendedAutomationBlock(tag, res, block, pbContentID, k) if err != nil { return nil, err } @@ -134,7 +185,7 @@ func (svc resourceTranslationsManager) pageExtended(ctx context.Context, res *ty return } -func (svc resourceTranslationsManager) pageExtendedAutomatinBlock(tag language.Tag, res *types.Page, block types.PageBlock, blockID uint64, k types.LocaleKey) (locale.ResourceTranslationSet, error) { +func (svc resourceTranslationsManager) pageExtendedAutomationBlock(tag language.Tag, res *types.Page, block types.PageBlock, blockID uint64, k types.LocaleKey) (locale.ResourceTranslationSet, error) { out := make(locale.ResourceTranslationSet, 0, 10) bb, _ := block.Options["buttons"].([]interface{}) diff --git a/compose/service/module.go b/compose/service/module.go index 3539d8bb6..01d641d71 100644 --- a/compose/service/module.go +++ b/compose/service/module.go @@ -145,15 +145,8 @@ func (svc module) Find(ctx context.Context, filter types.ModuleFilter) (set type return err } - // i18n - tag := locale.GetAcceptLanguageFromContext(ctx) set.Walk(func(m *types.Module) error { - m.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, m.ResourceTranslation())) - - m.Fields.Walk(func(mf *types.ModuleField) error { - mf.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, mf.ResourceTranslation())) - return nil - }) + svc.proc(ctx, m) return nil }) return nil @@ -220,6 +213,20 @@ func (svc module) FindByAny(ctx context.Context, namespaceID uint64, identifier return m, nil } +func (svc module) proc(ctx context.Context, m *types.Module) { + if svc.locale == nil || svc.locale.Locale() == nil { + return + } + + tag := locale.GetAcceptLanguageFromContext(ctx) + m.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, m.ResourceTranslation())) + + m.Fields.Walk(func(mf *types.ModuleField) error { + mf.DecodeTranslations(svc.locale.Locale().ResourceTranslations(tag, mf.ResourceTranslation())) + return nil + }) +} + func (svc module) Create(ctx context.Context, new *types.Module) (*types.Module, error) { var ( ns *types.Namespace @@ -444,9 +451,10 @@ func (svc module) lookup(ctx context.Context, namespaceID uint64, lookup func(*m } return loadModuleFields(ctx, svc.store, m) - }() + svc.proc(ctx, m) + return m, svc.recordAction(ctx, aProps, ModuleActionLookup, err) } @@ -485,11 +493,11 @@ func (svc module) handleUpdate(ctx context.Context, upd *types.Module) moduleUpd } // Get max validatorID for later use - vvID := make([]uint64, len(res.Fields)) - for i, f := range res.Fields { + vvID := make(map[uint64]uint64) + for _, f := range res.Fields { for _, v := range f.Expressions.Validators { - if vvID[i] < v.ValidatorID { - vvID[i] = v.ValidatorID + if vvID[f.ID] < v.ValidatorID { + vvID[f.ID] = v.ValidatorID } } } @@ -529,11 +537,11 @@ func (svc module) handleUpdate(ctx context.Context, upd *types.Module) moduleUpd } // Assure validatorIDs - for i, f := range res.Fields { + for _, f := range res.Fields { for j, v := range f.Expressions.Validators { if v.ValidatorID == 0 { - vvID[i] += 1 - v.ValidatorID = vvID[i] + vvID[f.ID] += 1 + v.ValidatorID = vvID[f.ID] f.Expressions.Validators[j] = v changes |= moduleFieldsChanged diff --git a/compose/types/locale.gen.go b/compose/types/locale.gen.go index aaa6e824e..3d33fceb1 100644 --- a/compose/types/locale.gen.go +++ b/compose/types/locale.gen.go @@ -14,8 +14,9 @@ package types import ( "fmt" - "github.com/cortezaproject/corteza-server/pkg/locale" "strconv" + + "github.com/cortezaproject/corteza-server/pkg/locale" ) type ( @@ -71,6 +72,12 @@ var ( Path: "expression.validator.{{validatorID}}.error", CustomHandler: "validatorError", } + LocaleKeyModuleFieldOptionsOptionTexts = LocaleKey{ + Name: "optionsOptionTexts", + Resource: ModuleFieldResourceTranslationType, + Path: "meta.options.{{value}}.text", + CustomHandler: "optionsOptionTexts", + } LocaleKeyModuleName = LocaleKey{ Name: "name", Resource: ModuleResourceTranslationType, @@ -154,6 +161,7 @@ func (r *ModuleField) DecodeTranslations(tt locale.ResourceTranslationIndex) { r.decodeTranslationsHintView(tt) r.decodeTranslationsHintEdit(tt) r.decodeTranslationsValidatorError(tt) + r.decodeTranslationsOptionsOptionTexts(tt) } func (r *ModuleField) EncodeTranslations() (out locale.ResourceTranslationSet) { @@ -171,6 +179,7 @@ func (r *ModuleField) EncodeTranslations() (out locale.ResourceTranslationSet) { out = append(out, r.encodeTranslationsHintView()...) out = append(out, r.encodeTranslationsHintEdit()...) out = append(out, r.encodeTranslationsValidatorError()...) + out = append(out, r.encodeTranslationsOptionsOptionTexts()...) return out } diff --git a/compose/types/module_field.go b/compose/types/module_field.go index e13428a02..2bd514705 100644 --- a/compose/types/module_field.go +++ b/compose/types/module_field.go @@ -103,6 +103,58 @@ func (f *ModuleField) decodeTranslationsHintEdit(tt locale.ResourceTranslationIn } } +// Decodes translations and modifies options +// +// Why "options-option-texts"? Because we're translating option txts under options key-value +func (f *ModuleField) decodeTranslationsOptionsOptionTexts(tt locale.ResourceTranslationIndex) { + var ( + tr *locale.ResourceTranslation + ) + + optsUnknown, has := f.Options["options"] + if !has { + return + } + + optsSlice, is := optsUnknown.([]interface{}) + if !is { + return + } + + for i, optUnknown := range optsSlice { + outOpt := map[string]string{} + + // what is this we're dealing with? slice of strings (values) or map (value+text) + switch opt := optUnknown.(type) { + case string: + // cast string (value) into map (value+text) + // and use value as text (as a fallback in case + // of missing translation) + outOpt["value"] = opt + outOpt["text"] = opt + + case map[string]interface{}: + outOpt["value"], is = opt["value"].(string) + if !is { + continue + } + + outOpt["text"], _ = opt["text"].(string) + } + + // find the translation for that value + // and update the option (effectively overwriting + // the original text value (in case of map option) + trKey := strings.NewReplacer("{{value}}", outOpt["value"]).Replace(LocaleKeyModuleFieldOptionsOptionTexts.Path) + if tr = tt.FindByKey(trKey); tr != nil { + outOpt["text"] = tr.Msg + } + + // Update slice item with translated option + optsSlice[i] = outOpt + } +} + func (m *ModuleField) encodeTranslationsValidatorError() (out locale.ResourceTranslationSet) { out = make(locale.ResourceTranslationSet, 0, 3) @@ -179,12 +231,45 @@ func (f *ModuleField) encodeTranslationsHintEdit() (out locale.ResourceTranslati return out } -func (m ModuleField) Clone() *ModuleField { - return &m +// extracts option texts and converts (encodes) them to translations +func (f *ModuleField) encodeTranslationsOptionsOptionTexts() (out locale.ResourceTranslationSet) { + out = make(locale.ResourceTranslationSet, 0, 3) + + optsUnknown, has := f.Options["options"] + if !has { + return + } + + optsSlice, is := optsUnknown.([]interface{}) + if !is { + return + } + + for _, optUnknown := range optsSlice { + // we only care about maps (items with value & text) + switch opt := optUnknown.(type) { + case map[string]interface{}: + value, _ := opt["value"].(string) + text, _ := opt["text"].(string) + + out = append(out, &locale.ResourceTranslation{ + Resource: f.ResourceTranslation(), + Key: strings.NewReplacer("{{value}}", value). + Replace(LocaleKeyModuleFieldOptionsOptionTexts.Path), + Msg: text, + }) + } + } + + return } -func (m ModuleField) setOptionKey(v interface{}, kk ...string) { - opt := m.Options +func (f ModuleField) Clone() *ModuleField { + return &f +} + +func (f ModuleField) setOptionKey(v interface{}, kk ...string) { + opt := f.Options for _, k := range kk[0 : len(kk)-1] { _, ok := opt[k] @@ -206,8 +291,8 @@ func (m ModuleField) setOptionKey(v interface{}, kk ...string) { opt[k] = v } -func (m ModuleField) getOptionKey(kk ...string) interface{} { - opt := m.Options +func (f ModuleField) getOptionKey(kk ...string) interface{} { + opt := f.Options for _, k := range kk[0 : len(kk)-1] { _, ok := opt[k] diff --git a/compose/types/module_field_options.go b/compose/types/module_field_options.go index 3261d9023..35492bae2 100644 --- a/compose/types/module_field_options.go +++ b/compose/types/module_field_options.go @@ -19,6 +19,8 @@ const ( moduleFieldOptionIsUnique = "isUnique" moduleFieldOptionIsUniqueMultiValue = "isUniqueMultiValue" + moduleFieldOptionOptions = "options" + moduleFieldNumberOptionPrecision = "precision" moduleFieldNumberOptionPrecisionMin uint = 0 moduleFieldNumberOptionPrecisionMax uint = 6 diff --git a/compose/types/module_field_test.go b/compose/types/module_field_test.go new file mode 100644 index 000000000..8746de13e --- /dev/null +++ b/compose/types/module_field_test.go @@ -0,0 +1,78 @@ +package types + +import ( + "testing" + + "github.com/cortezaproject/corteza-server/pkg/locale" + "github.com/stretchr/testify/require" +) + +func TestModuleField_decodeTranslationsOptionsOptionTexts(t *testing.T) { + rti := locale.ResourceTranslationIndex{ + "meta.options.val1.text": &locale.ResourceTranslation{Msg: "TEXT-1"}, + } + tests := []struct { + name string + opts ModuleFieldOptions + out interface{} + }{ + // TODO: Add test cases. + {"all-nil", nil, nil}, + {"options-nil", + ModuleFieldOptions{ + "options": nil, + }, + nil, + }, + {"options-foo", + ModuleFieldOptions{ + "options": "foo", + }, + nil, + }, + {"options-foo-number", + ModuleFieldOptions{ + "options": []interface{}{1}, + }, + nil, + }, + {"options-valid-slice", + ModuleFieldOptions{ + "options": []interface{}{ + "val1", + "val2", + }, + }, + []interface{}{ + map[string]string{"value": "val1", "text": "TEXT-1"}, + map[string]string{"value": "val2", "text": "val2"}, + }, + }, + {"options-valid-map", + ModuleFieldOptions{ + "options": []interface{}{ + map[string]interface{}{"value": "val1", "text": "Text1"}, + map[string]interface{}{"value": "val2", "text": "Text2"}, + }, + }, + []interface{}{ + map[string]string{"value": "val1", "text": "TEXT-1"}, + map[string]string{"value": "val2", "text": "Text2"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + req = require.New(t) + f = &ModuleField{Options: tt.opts} + ) + + f.decodeTranslationsOptionsOptionTexts(rti) + + if tt.out != nil { + req.Equal(tt.out, f.Options["options"]) + } + }) + } +} diff --git a/compose/types/type_labels.gen.go b/compose/types/type_labels.gen.go index 22e9ab5c7..33c6ab529 100644 --- a/compose/types/type_labels.gen.go +++ b/compose/types/type_labels.gen.go @@ -57,17 +57,17 @@ func (m Module) LabelResourceID() uint64 { } // SetLabel adds new label to label map -func (m *ModuleField) SetLabel(key string, value string) { - if m.Labels == nil { - m.Labels = make(map[string]string) +func (f *ModuleField) SetLabel(key string, value string) { + if f.Labels == nil { + f.Labels = make(map[string]string) } - m.Labels[key] = value + f.Labels[key] = value } // GetLabels adds new label to label map -func (m ModuleField) GetLabels() map[string]string { - return m.Labels +func (f ModuleField) GetLabels() map[string]string { + return f.Labels } // GetLabels adds new label to label map @@ -76,8 +76,8 @@ func (ModuleField) LabelResourceKind() string { } // GetLabels adds new label to label map -func (m ModuleField) LabelResourceID() uint64 { - return m.ID +func (f ModuleField) LabelResourceID() uint64 { + return f.ID } // SetLabel adds new label to label map diff --git a/def/compose.module-field.yaml b/def/compose.module-field.yaml index 0277e2b33..b22fb9586 100644 --- a/def/compose.module-field.yaml +++ b/def/compose.module-field.yaml @@ -22,3 +22,8 @@ locale: - { name: hintView, path: meta.hint.view, custom: true, customHandler: hintView } - { name: hintEdit, path: meta.hint.edit, custom: true, customHandler: hintEdit } - { name: validatorError, path: "expression.validator.{{validatorID}}.error", custom: true, customHandler: validatorError } + - { name: optionsOptionTexts, + path: "meta.options.{{value}}.text", + custom: true, + customHandler: optionsOptionTexts + }