Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework template translation finding #1197

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions assets/static/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,15 @@ func (t *Template) Translations() []assets.TemplateTranslation {

// TemplateTranslation represents a single template translation
type TemplateTranslation struct {
Channel_ assets.ChannelReference `json:"channel" validate:"required"`
Content_ string `json:"content" validate:"required"`
Locale_ i18n.Locale `json:"locale" validate:"required"`
Namespace_ string `json:"namespace"`
VariableCount_ int `json:"variable_count"`
Channel_ *assets.ChannelReference `json:"channel" validate:"required"`
Content_ string `json:"content" validate:"required"`
Locale_ i18n.Locale `json:"locale" validate:"required"`
Namespace_ string `json:"namespace"`
VariableCount_ int `json:"variable_count"`
}

// NewTemplateTranslation creates a new template translation
func NewTemplateTranslation(channel assets.ChannelReference, locale i18n.Locale, content string, variableCount int, namespace string) *TemplateTranslation {
func NewTemplateTranslation(channel *assets.ChannelReference, locale i18n.Locale, content string, variableCount int, namespace string) *TemplateTranslation {
return &TemplateTranslation{
Channel_: channel,
Content_: content,
Expand All @@ -69,4 +69,4 @@ func (t *TemplateTranslation) Locale() i18n.Locale { return t.Locale_ }
func (t *TemplateTranslation) VariableCount() int { return t.VariableCount_ }

// Channel returns the channel this template translation is for
func (t *TemplateTranslation) Channel() assets.ChannelReference { return t.Channel_ }
func (t *TemplateTranslation) Channel() *assets.ChannelReference { return t.Channel_ }
5 changes: 1 addition & 4 deletions assets/static/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import (
)

func TestTemplate(t *testing.T) {
channel := assets.ChannelReference{
Name: "Test Channel",
UUID: assets.ChannelUUID("ffffffff-9b24-92e1-ffff-ffffb207cdb4"),
}
channel := assets.NewChannelReference("Test Channel", "ffffffff-9b24-92e1-ffff-ffffb207cdb4")

translation := NewTemplateTranslation(channel, i18n.Locale("eng-US"), "Hello {{1}}", 1, "0162a7f4_dfe4_4c96_be07_854d5dba3b2b")
assert.Equal(t, channel, translation.Channel())
Expand Down
2 changes: 1 addition & 1 deletion assets/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ type TemplateTranslation interface {
Locale() i18n.Locale
Namespace() string
VariableCount() int
Channel() ChannelReference
Channel() *ChannelReference
}

// TemplateReference is used to reference a Template
Expand Down
9 changes: 7 additions & 2 deletions flows/actions/send_msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,26 @@ func (a *SendMsgAction) Execute(run flows.Run, step flows.Step, logModifier flow

sa := run.Session().Assets()

var template *flows.Template
if a.Templating != nil {
template = sa.Templates().Get(a.Templating.Template.UUID)
}

// create a new message for each URN+channel destination
for _, dest := range destinations {
urn := dest.URN.URN()
channelRef := assets.NewChannelReference(dest.Channel.UUID(), dest.Channel.Name())

// do we have a template defined?
var templating *flows.MsgTemplating
if a.Templating != nil {
if template != nil {
// looks for a translation in the contact locale or environment default
locales := []i18n.Locale{
run.Session().MergedEnvironment().DefaultLocale(),
run.Session().Environment().DefaultLocale(),
}

translation := sa.Templates().FindTranslation(a.Templating.Template.UUID, channelRef, locales)
translation := template.FindTranslation(dest.Channel, locales)
if translation != nil {
localizedVariables, _ := run.GetTextArray(uuids.UUID(a.Templating.UUID), "variables", a.Templating.Variables, nil)

Expand Down
57 changes: 11 additions & 46 deletions flows/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,38 +31,22 @@ func (t *Template) Reference() *assets.TemplateReference {
}

// FindTranslation finds the matching translation for the passed in channel and languages (in priority order)
func (t *Template) FindTranslation(channel assets.ChannelUUID, locales []i18n.Locale) *TemplateTranslation {
// first iterate through and find all translations that are for this channel
candidatesByLocale := make(map[i18n.Locale]*TemplateTranslation)
candidatesByLang := make(map[i18n.Language]*TemplateTranslation)
func (t *Template) FindTranslation(channel *Channel, locales []i18n.Locale) *TemplateTranslation {
// find all translations for this channel
candidates := make(map[string]*TemplateTranslation)
candidateLocales := make([]string, 0, 5)
for _, tr := range t.Template.Translations() {
if tr.Channel().UUID == channel {
tt := NewTemplateTranslation(tr)
lang, _ := tt.Locale().Split()

candidatesByLocale[tt.Locale()] = tt
candidatesByLang[lang] = tt
}
}

// first look for exact locale match
for _, locale := range locales {
tt := candidatesByLocale[locale]
if tt != nil {
return tt
if tr.Channel().UUID == channel.UUID() {
candidates[string(tr.Locale())] = NewTemplateTranslation(tr)
candidateLocales = append(candidateLocales, string(tr.Locale()))
}
}

// if that fails look for language match
for _, locale := range locales {
lang, _ := locale.Split()
tt := candidatesByLang[lang]
if tt != nil {
return tt
}
if len(candidates) == 0 {
return nil
}

return nil
match := i18n.NewBCP47Matcher(candidateLocales...).ForLocales(locales...)
return candidates[match]
}

// TemplateTranslation represents a single translation for a template
Expand Down Expand Up @@ -119,22 +103,3 @@ func NewTemplateAssets(ts []assets.Template) *TemplateAssets {
func (a *TemplateAssets) Get(uuid assets.TemplateUUID) *Template {
return a.byUUID[uuid]
}

// FindTranslation looks through our list of templates to find the template matching the passed in uuid
// If no template or translation is found then empty string is returned
func (a *TemplateAssets) FindTranslation(uuid assets.TemplateUUID, channel *assets.ChannelReference, locales []i18n.Locale) *TemplateTranslation {
// no channel, can't match to a template
if channel == nil {
return nil
}

template := a.byUUID[uuid]

// not found, no template
if template == nil {
return nil
}

// look through our translations looking for a match by both channel and translation
return template.FindTranslation(channel.UUID, locales)
}
73 changes: 38 additions & 35 deletions flows/template_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package flows
package flows_test

import (
"fmt"
"testing"

"github.com/nyaruka/gocommon/i18n"
"github.com/nyaruka/goflow/assets"
"github.com/nyaruka/goflow/assets/static"
"github.com/nyaruka/goflow/flows"
"github.com/nyaruka/goflow/test"
"github.com/stretchr/testify/assert"
)

Expand All @@ -23,85 +26,85 @@ func TestTemplateTranslation(t *testing.T) {
channel := assets.NewChannelReference("0bce5fd3-c215-45a0-bcb8-2386eb194175", "Test Channel")

for i, tc := range tcs {
tt := NewTemplateTranslation(static.NewTemplateTranslation(*channel, i18n.Locale("eng-US"), tc.Content, len(tc.Variables), "a6a8863e_7879_4487_ad24_5e2ea429027c"))
tt := flows.NewTemplateTranslation(static.NewTemplateTranslation(channel, i18n.Locale("eng-US"), tc.Content, len(tc.Variables), "a6a8863e_7879_4487_ad24_5e2ea429027c"))
result := tt.Substitute(tc.Variables)
assert.Equal(t, tc.Expected, result, "%d: unexpected template substitution", i)
}
}

func TestTemplates(t *testing.T) {
channel1 := assets.NewChannelReference("0bce5fd3-c215-45a0-bcb8-2386eb194175", "Test Channel")
tt1 := static.NewTemplateTranslation(*channel1, i18n.Locale("eng"), "Hello {{1}}", 1, "")
tt2 := static.NewTemplateTranslation(*channel1, i18n.Locale("spa-EC"), "Que tal {{1}}", 1, "")
tt3 := static.NewTemplateTranslation(*channel1, i18n.Locale("spa-ES"), "Hola {{1}}", 1, "")
template := NewTemplate(static.NewTemplate("c520cbda-e118-440f-aaf6-c0485088384f", "greeting", []*static.TemplateTranslation{tt1, tt2, tt3}))
func TestTemplate(t *testing.T) {
channel1 := test.NewChannel("WhatsApp 1", "+12345", []string{"whatsapp"}, []assets.ChannelRole{}, nil)
channel2 := test.NewChannel("WhatsApp 2", "+23456", []string{"whatsapp"}, []assets.ChannelRole{}, nil)
channel3 := test.NewChannel("WhatsApp 3", "+34567", []string{"whatsapp"}, []assets.ChannelRole{}, nil)
channel1Ref := assets.NewChannelReference(channel1.UUID(), channel1.Name())
channel2Ref := assets.NewChannelReference(channel2.UUID(), channel2.Name())

tas := NewTemplateAssets([]assets.Template{template})
tt1 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("eng"), "Hello {{1}}", 1, "")
tt2 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-EC"), "Que tal {{1}}", 1, "")
tt3 := static.NewTemplateTranslation(channel1Ref, i18n.Locale("spa-ES"), "Hola {{1}}", 1, "")
tt4 := static.NewTemplateTranslation(channel2Ref, i18n.Locale("en"), "Hello {{1}}", 1, "")
template := flows.NewTemplate(static.NewTemplate("c520cbda-e118-440f-aaf6-c0485088384f", "greeting", []*static.TemplateTranslation{tt1, tt2, tt3, tt4}))

tas := flows.NewTemplateAssets([]assets.Template{template})

tcs := []struct {
UUID assets.TemplateUUID
Channel *assets.ChannelReference
Locales []i18n.Locale
Variables []string
Expected string
channel *flows.Channel
locales []i18n.Locale
variables []string
expected string
}{
{
"c520cbda-e118-440f-aaf6-c0485088384f",
channel1,
[]i18n.Locale{"eng-US", "spa-CO"},
[]string{"Chef"},
"Hello Chef",
},
{
"c520cbda-e118-440f-aaf6-c0485088384f",
channel1,
[]i18n.Locale{"eng", "spa-CO"},
[]string{"Chef"},
"Hello Chef",
},
{
"c520cbda-e118-440f-aaf6-c0485088384f",
channel1,
[]i18n.Locale{"deu-DE", "spa-ES"},
[]string{"Chef"},
"Hola Chef",
},
{
"c520cbda-e118-440f-aaf6-c0485088384f",
nil,
[]i18n.Locale{"deu-DE", "spa-ES"},
channel1,
[]i18n.Locale{"deu-DE"},
[]string{"Chef"},
"",
"Hello Chef",
},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this case is a little weird because it's just defaulting to the first translation... but it does seem better to generally return something here than creating a message without a template that probably going to fail.

{
"c520cbda-e118-440f-aaf6-c0485088384f",
channel1,
[]i18n.Locale{"deu-DE"},
channel2,
[]i18n.Locale{"eng-US", "spa-ES"},
[]string{"Chef"},
"",
"Hello Chef",
},
{
"8c5d4910-114a-4521-ba1d-bde8b024865a",
channel1,
channel3,
[]i18n.Locale{"eng-US", "spa-ES"},
[]string{"Chef"},
"",
},
}

for _, tc := range tcs {
tr := tas.FindTranslation(tc.UUID, tc.Channel, tc.Locales)
if tr == nil {
assert.Equal(t, "", tc.Expected)
continue
testID := fmt.Sprintf("channel '%s' and locales %v", tc.channel.Name(), tc.locales)
tr := template.FindTranslation(tc.channel, tc.locales)
if tc.expected == "" {
assert.Nil(t, tr, "unexpected translation found for %s", testID)
} else {
if assert.NotNil(t, tr, "expected translation to be found for %s", testID) {
assert.Equal(t, tc.expected, tr.Substitute(tc.variables), "substition mismatch for %s", testID)
}
}
assert.NotNil(t, tr.Asset())

assert.Equal(t, tc.Expected, tr.Substitute(tc.Variables))
}

template = tas.Get(assets.TemplateUUID("c520cbda-e118-440f-aaf6-c0485088384f"))
assert.NotNil(t, template)
assert.Equal(t, assets.NewTemplateReference("c520cbda-e118-440f-aaf6-c0485088384f", "greeting"), template.Reference())
assert.Equal(t, (*assets.TemplateReference)(nil), (*Template)(nil).Reference())
assert.Equal(t, (*assets.TemplateReference)(nil), (*flows.Template)(nil).Reference())
}
Loading