diff --git a/cmd/flowrunner/testdata/flows/two_questions.json b/cmd/flowrunner/testdata/flows/two_questions.json index f0567cca6..aab97e225 100644 --- a/cmd/flowrunner/testdata/flows/two_questions.json +++ b/cmd/flowrunner/testdata/flows/two_questions.json @@ -6,43 +6,69 @@ "localization": { "fra": { "e97cd6d5-3354-4dbd-85bc-6c1f87849eec": { - "text": "Quelle est votres couleur preferee? (rouge/blue)" + "text": [ + "Quelle est votres couleur preferee? (rouge/blue)" + ] }, "98503572-25bf-40ce-ad72-8836b6549a38": { - "test": "rouge" + "test": [ + "rouge" + ] }, "a51e5c8c-c891-401d-9c62-15fc37278c94": { - "test": "blue" + "test": [ + "blue" + ] }, "598ae7a5-2f81-48f1-afac-595262514aa1": { - "label": "Rouge" + "name": [ + "Rouge" + ] }, "c70fe86c-9aac-4cc2-a5cb-d35cbe3fed6e": { - "label": "Blue" + "name": [ + "Blue" + ] }, "78ae8f05-f92e-43b2-a886-406eaea1b8e0": { - "label": "Autres" + "name": [ + "Autres" + ] }, "d2a4052a-3fa9-4608-ab3e-5b9631440447": { - "text": "@(TITLE(input.text))! Bien sur! Quelle est votes soda preferee? (pepsi/coke)" + "text": [ + "@(TITLE(input.text))! Bien sur! Quelle est votes soda preferee? (pepsi/coke)" + ] }, "e27c3bce-1095-4d08-9164-dc4530a0688a": { - "test": "pepsi" + "test": [ + "pepsi" + ] }, "4a6c3b0b-0658-4a93-ae37-bee68f6a6a87": { - "test": "coke" + "test": [ + "coke" + ] }, "2ab9b033-77a8-4e56-a558-b568c00c9492": { - "label": "Pepsi" + "label": [ + "Pepsi" + ] }, "c7bca181-0cb3-4ec6-8555-f7e5644238ad": { - "label": "Coke" + "label": [ + "Coke" + ] }, "5ce6c69a-fdfe-4594-ab71-26be534d31c3": { - "label": "Autres" + "label": [ + "Autres" + ] }, "0a8467eb-911a-41db-8101-ccf415c48e6a": { - "text": "Parfait, vous avez finis et tu aimes @run.results.soda.category" + "text": [ + "Parfait, vous avez finis et tu aimes @run.results.soda.category" + ] } } }, diff --git a/excellent/legacy.go b/excellent/legacy.go index 930bdfd67..950551cdd 100644 --- a/excellent/legacy.go +++ b/excellent/legacy.go @@ -40,22 +40,33 @@ func (v vars) String() string { } type arbitraryVars struct { - vars vars - base string - nesting string + vars vars + base string + nesting string + nestedVars vars } func (v arbitraryVars) Resolve(key string) interface{} { + value, ok := v.vars[key] if ok { return fmt.Sprintf("%s.%s", v.base, value) } + prefix := v.base if v.nesting != "" { - return fmt.Sprintf("%s.%s.%s", v.base, v.nesting, key) + prefix = fmt.Sprintf("%s.%s", v.base, v.nesting) + } + + if v.nestedVars != nil { + return &arbitraryVars{ + base: fmt.Sprintf("%s.%s", prefix, key), + vars: v.nestedVars, + } } - return fmt.Sprintf("%s.%s", v.base, key) + return fmt.Sprintf("%s.%s", prefix, key) + } func (v arbitraryVars) Default() interface{} { @@ -140,6 +151,9 @@ func newVars() vars { }, "flow": arbitraryVars{ base: "run.results", + nestedVars: map[string]interface{}{ + "category": "category_localized", + }, }, "step": vars{ "value": "input.text", diff --git a/excellent/legacy_test.go b/excellent/legacy_test.go index 04b85f6e1..93d1aba1c 100644 --- a/excellent/legacy_test.go +++ b/excellent/legacy_test.go @@ -51,7 +51,7 @@ func TestTranslate(t *testing.T) { {old: "@contact.uuid", new: "@contact.uuid"}, {old: "@contact.blerg", new: "@contact.fields.blerg"}, {old: "@flow.blerg", new: "@run.results.blerg"}, - {old: "@flow.blerg.category", new: "@run.results.blerg.category"}, + {old: "@flow.blerg.category", new: "@run.results.blerg.category_localized"}, {old: "@step.value", new: "@input.text"}, {old: "@step.contact", new: "@step.contact"}, {old: "@date.now", new: "@(now())"}, diff --git a/flows/actions/save_flow_result.go b/flows/actions/save_flow_result.go index d8931db9c..04650ce8a 100644 --- a/flows/actions/save_flow_result.go +++ b/flows/actions/save_flow_result.go @@ -55,17 +55,20 @@ func (a *SaveFlowResultAction) Execute(run flows.FlowRun, step flows.Step) error } template = run.GetText(flows.UUID(a.UUID), "category", a.Category) - category, err := excellent.EvaluateTemplateAsString(run.Environment(), run.Context(), template) + categoryLocalized, err := excellent.EvaluateTemplateAsString(run.Environment(), run.Context(), template) if err != nil { run.AddError(step, err) } - // log our event - event := events.NewSaveFlowResult(step.NodeUUID(), a.ResultName, value, category) + if a.Category == categoryLocalized { + categoryLocalized = "" + } + + event := events.NewSaveFlowResult(step.NodeUUID(), a.ResultName, value, a.Category, categoryLocalized) run.AddEvent(step, event) // and save our result - run.Results().Save(step.NodeUUID(), a.ResultName, value, a.Category, *event.CreatedOn()) + run.Results().Save(step.NodeUUID(), a.ResultName, value, a.Category, categoryLocalized, *event.CreatedOn()) return nil } diff --git a/flows/definition/legacy.go b/flows/definition/legacy.go index 37b1f8e5e..8a5c012eb 100644 --- a/flows/definition/legacy.go +++ b/flows/definition/legacy.go @@ -14,7 +14,8 @@ import ( "github.com/satori/go.uuid" ) -type legacyFlow struct { +// LegacyFlow imports an old-world flow so it can be exported anew +type LegacyFlow struct { flow envelope legacyFlowEnvelope } @@ -130,8 +131,8 @@ type stringTest struct { type localizations map[utils.Language]flows.Action // ReadLegacyFlows reads in legacy formatted flows -func ReadLegacyFlows(data json.RawMessage) ([]legacyFlow, error) { - var flows []legacyFlow +func ReadLegacyFlows(data json.RawMessage) ([]LegacyFlow, error) { + var flows []LegacyFlow err := json.Unmarshal(data, &flows) return flows, err } @@ -142,7 +143,7 @@ func addTranslationMap(baseLanguage utils.Language, translations *flowTranslatio for language, translation := range mapped { items := itemTranslations{} expression, _ := excellent.TranslateTemplate(translation) - items[key] = expression + items[key] = []string{expression} if language != baseLanguage { addTranslation(baseLanguage, translations, language, uuid, items) } @@ -253,17 +254,16 @@ func createAction(baseLanguage utils.Language, a legacyAction, fieldMap map[stri addTranslationMap(baseLanguage, translations, msg, flows.UUID(a.UUID), "text") // TODO translations for each attachment? - - text_expression, _ := excellent.TranslateTemplate(msg[baseLanguage]) - attachment_expression, _ := excellent.TranslateTemplate(media[baseLanguage]) + textExpression, _ := excellent.TranslateTemplate(msg[baseLanguage]) + attachmentExpression, _ := excellent.TranslateTemplate(media[baseLanguage]) attachments := []string{} - if attachment_expression != "" { - attachments = append(attachments, attachment_expression) + if attachmentExpression != "" { + attachments = append(attachments, attachmentExpression) } return &actions.ReplyAction{ - Text: text_expression, + Text: textExpression, Attachments: attachments, BaseAction: actions.BaseAction{ UUID: a.UUID, @@ -419,7 +419,7 @@ func parseRules(baseLanguage utils.Language, r legacyRuleSet, translations *flow exits := make([]flows.Exit, len(categoryMap)) exitMap := make(map[string]flows.Exit) for k, category := range categoryMap { - addTranslationMap(baseLanguage, translations, category.translations, flows.UUID(category.uuid), "label") + addTranslationMap(baseLanguage, translations, category.translations, flows.UUID(category.uuid), "name") exits[category.order] = &exit{ name: k, @@ -559,13 +559,14 @@ func createActionNode(lang utils.Language, a legacyActionSet, fieldMap map[strin node.exits = make([]flows.Exit, 1) node.exits[0] = &exit{ destination: a.Destination, - uuid: flows.ExitUUID(a.UUID), + uuid: flows.ExitUUID(uuid.NewV4().String()), } return node } -func (f *legacyFlow) UnmarshalJSON(data []byte) error { +// UnmarshalJSON imports our JSON into a LegacyFlow object +func (f *LegacyFlow) UnmarshalJSON(data []byte) error { var envelope legacyFlowEnvelope var err error @@ -611,7 +612,8 @@ func (f *legacyFlow) UnmarshalJSON(data []byte) error { return err } -func (f *legacyFlow) MarshalJSON() ([]byte, error) { +// MarshalJSON sends turns our legacy flow into bytes +func (f *LegacyFlow) MarshalJSON() ([]byte, error) { var fe = flowEnvelope{} fe.Name = f.name diff --git a/flows/definition/localization.go b/flows/definition/localization.go index 8bebb63b7..acd1de92b 100644 --- a/flows/definition/localization.go +++ b/flows/definition/localization.go @@ -5,13 +5,13 @@ import ( "github.com/nyaruka/goflow/utils" ) -// itemTranslations map a key for a node to a key - say "text" to "je suis francais!" -type itemTranslations map[string]string +// itemTranslations map a key for a node to a key - say "text" to "[je suis francais!]" +type itemTranslations map[string][]string // languageTranslations map a node uuid to item_translations - say "node1-asdf" to { "text": "je suis francais!" } type languageTranslations map[flows.UUID]itemTranslations -func (t *languageTranslations) GetText(uuid flows.UUID, key string, backdown string) string { +func (t *languageTranslations) GetTranslations(uuid flows.UUID, key string, backdown []string) []string { item, found := (*t)[uuid] if found { translation, found := item[key] @@ -22,6 +22,17 @@ func (t *languageTranslations) GetText(uuid flows.UUID, key string, backdown str return backdown } +func (t *languageTranslations) GetText(uuid flows.UUID, key string, backdown string) string { + item, found := (*t)[uuid] + if found { + translation, found := item[key] + if found && len(translation) > 0 { + return translation[0] + } + } + return backdown +} + // flowTranslations are our top level container for all the translations for a language type flowTranslations map[utils.Language]*languageTranslations diff --git a/flows/engine/engine.go b/flows/engine/engine.go index e23ba88bd..4456a228b 100644 --- a/flows/engine/engine.go +++ b/flows/engine/engine.go @@ -265,7 +265,11 @@ func pickNodeExit(run flows.FlowRun, node flows.Node, step flows.Step) (flows.No // find our exit for _, e := range node.Exits() { if e.UUID() == exitUUID { - exitName = e.Name() + + localizedName := run.GetText(flows.UUID(exitUUID), "name", e.Name()) + if localizedName != e.Name() { + exitName = localizedName + } exit = e break } @@ -277,9 +281,9 @@ func pickNodeExit(run flows.FlowRun, node flows.Node, step flows.Step) (flows.No // save our results if appropriate if router != nil && router.ResultName() != "" { - event := events.NewSaveFlowResult(node.UUID(), router.ResultName(), route.Match(), exitName) + event := events.NewSaveFlowResult(node.UUID(), router.ResultName(), route.Match(), exit.Name(), exitName) run.AddEvent(step, event) - run.Results().Save(node.UUID(), router.ResultName(), route.Match(), exitName, *event.CreatedOn()) + run.Results().Save(node.UUID(), router.ResultName(), route.Match(), exit.Name(), exitName, *event.CreatedOn()) } // log any error we received diff --git a/flows/events/save_flow_result.go b/flows/events/save_flow_result.go index a6e588c7c..fb94fa97f 100644 --- a/flows/events/save_flow_result.go +++ b/flows/events/save_flow_result.go @@ -24,15 +24,16 @@ const TypeSaveFlowResult string = "save_flow_result" // @event save_flow_result type SaveFlowResultEvent struct { BaseEvent - NodeUUID flows.NodeUUID `json:"node_uuid" validate:"required"` - ResultName string `json:"result_name" validate:"required"` - Value string `json:"value"` - Category string `json:"category"` + NodeUUID flows.NodeUUID `json:"node_uuid" validate:"required"` + ResultName string `json:"result_name" validate:"required"` + Value string `json:"value"` + Category string `json:"category"` + CategoryLocalized string `json:"category_localized,omitempty"` } // NewSaveFlowResult returns a new save result event for the passed in values -func NewSaveFlowResult(node flows.NodeUUID, name string, value string, category string) *SaveFlowResultEvent { - return &SaveFlowResultEvent{NodeUUID: node, ResultName: name, Value: value, Category: category} +func NewSaveFlowResult(node flows.NodeUUID, name string, value string, categoryName string, categoryLocalized string) *SaveFlowResultEvent { + return &SaveFlowResultEvent{NodeUUID: node, ResultName: name, Value: value, Category: categoryName, CategoryLocalized: categoryLocalized} } // Type returns the type of this event diff --git a/flows/interfaces.go b/flows/interfaces.go index f62cc6f86..61c7dc978 100644 --- a/flows/interfaces.go +++ b/flows/interfaces.go @@ -154,6 +154,7 @@ type FlowTranslations interface { // Translations provide a way to get the translation for a specific language for a uuid/key pair type Translations interface { GetText(uuid UUID, key string, backdown string) string + GetTranslations(uuid UUID, key string, backdown []string) []string } type Context interface { @@ -249,6 +250,7 @@ type FlowRun interface { SetLanguage(utils.Language) SetFlowTranslations(FlowTranslations) GetText(uuid UUID, key string, backdown string) string + GetTranslations(uuid UUID, key string, backdown []string) []string Webhook() *utils.RequestResponse SetWebhook(*utils.RequestResponse) diff --git a/flows/results.go b/flows/results.go index cb4209396..3ea3a2665 100644 --- a/flows/results.go +++ b/flows/results.go @@ -20,8 +20,8 @@ type Results struct { } // Save saves a new result in our map. The key is saved in a snakified format -func (r *Results) Save(node NodeUUID, name string, value string, category string, createdOn time.Time) { - result := Result{node, name, value, category, createdOn} +func (r *Results) Save(node NodeUUID, name string, value string, category string, categoryLocalized string, createdOn time.Time) { + result := Result{node, name, value, category, categoryLocalized, createdOn} r.results[utils.Snakify(name)] = &result } @@ -55,11 +55,12 @@ var _ utils.VariableResolver = (*Results)(nil) // Result represents a result value in our flow run. Results have a name for which they are the result for, // the value itself of the result, optional category and the date and node the result was collected on type Result struct { - node NodeUUID - name string - value string - category string - createdOn time.Time + node NodeUUID + name string + value string + category string + categoryLocalized string + createdOn time.Time } // Resolve resolves the passed in key to a value. Result values have a name, value, category, node and created_on @@ -68,6 +69,11 @@ func (r *Result) Resolve(key string) interface{} { case "category": return r.category + case "category_localized": + if r.categoryLocalized == "" { + return r.category + } + return r.categoryLocalized case "created_on": return r.createdOn @@ -129,11 +135,12 @@ func (r *Results) MarshalJSON() ([]byte, error) { } type resultEnvelope struct { - Node NodeUUID `json:"node_uuid"` - Name string `json:"result_name"` - Value string `json:"value"` - Category string `json:"category,omitempty"` - CreatedOn time.Time `json:"created_on"` + Node NodeUUID `json:"node_uuid"` + Name string `json:"result_name"` + Value string `json:"value"` + Category string `json:"category,omitempty"` + CategoryLocalized string `json:"category_localized,omitempty"` + CreatedOn time.Time `json:"created_on"` } // UnmarshalJSON is our custom unmarshalling of a Result object @@ -146,6 +153,7 @@ func (r *Result) UnmarshalJSON(data []byte) error { r.name = re.Name r.value = re.Value r.category = re.Category + r.categoryLocalized = re.CategoryLocalized r.createdOn = re.CreatedOn return err @@ -159,6 +167,7 @@ func (r *Result) MarshalJSON() ([]byte, error) { re.Name = r.name re.Value = r.value re.Category = r.category + re.CategoryLocalized = r.categoryLocalized re.CreatedOn = r.createdOn return json.Marshal(re) diff --git a/flows/routers/switch.go b/flows/routers/switch.go index 3db4568b5..73e2899e2 100644 --- a/flows/routers/switch.go +++ b/flows/routers/switch.go @@ -83,8 +83,10 @@ func (r *SwitchRouter) PickRoute(run flows.FlowRun, exits []flows.Exit, step flo // build our argument list args := make([]interface{}, len(c.Arguments)+1) args[0] = operand + + localizedArgs := run.GetTranslations(c.UUID, "arguments", c.Arguments) for i := range c.Arguments { - test := run.GetText(c.UUID, fmt.Sprintf("args.%d", i), c.Arguments[i]) + test := localizedArgs[i] args[i+1], err = excellent.EvaluateTemplate(env, run.Context(), test) if err != nil { run.AddError(step, err) diff --git a/flows/runs/run.go b/flows/runs/run.go index 1316c2f62..eec521d02 100644 --- a/flows/runs/run.go +++ b/flows/runs/run.go @@ -219,6 +219,13 @@ func (r *flowRun) GetText(uuid flows.UUID, key string, backdown string) string { return r.translations.GetText(uuid, key, backdown) } +func (r *flowRun) GetTranslations(uuid flows.UUID, key string, backdown []string) []string { + if r.translations == nil { + return backdown + } + return r.translations.GetTranslations(uuid, key, backdown) +} + // NewRun initializes a new context and flow run for the passed in flow and contact func NewRun(env flows.FlowEnvironment, flow flows.Flow, contact *flows.Contact, parent flows.FlowRun) flows.FlowRun { now := time.Now().UTC()