Skip to content

Commit

Permalink
Escape data passed to templated translation messages
Browse files Browse the repository at this point in the history
  • Loading branch information
hawx committed Jan 18, 2024
1 parent 0f5f93c commit 974aec4
Show file tree
Hide file tree
Showing 15 changed files with 440 additions and 149 deletions.
2 changes: 1 addition & 1 deletion cmd/event-received/cloud_watch_event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (h *cloudWatchEventHandler) Handle(ctx context.Context, event events.CloudW
return handleEvidenceReceived(ctx, h.dynamoClient, event)

case "reduced-fee-approved":
bundle := localize.NewBundle("./lang/en.json", "./lang/cy.json")
bundle, _ := localize.NewBundle("./lang/en.json", "./lang/cy.json")

//TODO do this in handleFeeApproved when/if we save lang preference in LPA
appData := page.AppData{Localizer: bundle.For(localize.En)}
Expand Down
5 changes: 4 additions & 1 deletion cmd/mlpa/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ func main() {
logger.Fatal(err)
}

bundle := localize.NewBundle("lang/en.json", "lang/cy.json")
bundle, err := localize.NewBundle("lang/en.json", "lang/cy.json")
if err != nil {
logger.Fatal(err)
}

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
Expand Down
143 changes: 143 additions & 0 deletions internal/localize/bundle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package localize

import (
"encoding/json"
"errors"
"fmt"
"os"
"path"
"strings"
)

type parsedMessage struct {
S string

// when plural
One string
Other string

// for Welsh only
Two string
Few string
Many string
}

func (m *parsedMessage) UnmarshalJSON(text []byte) error {
var s string
if err := json.Unmarshal(text, &s); err == nil {
m.S = s
return nil
}

var v map[string]string
if err := json.Unmarshal(text, &v); err == nil {
m.One = v["one"]
m.Other = v["other"]
m.Two = v["two"]
m.Few = v["few"]
m.Many = v["many"]
return nil
}

return errors.New("message malformed")
}

type Bundle struct {
messages map[string]Messages
}

func NewBundle(paths ...string) (*Bundle, error) {
bundle := &Bundle{messages: map[string]Messages{}}

for _, path := range paths {
if err := bundle.LoadMessageFile(path); err != nil {
return nil, err
}
}

return bundle, nil
}

func (b *Bundle) LoadMessageFile(p string) error {
data, err := os.ReadFile(p)
if err != nil {
return err
}

var v map[string]parsedMessage
if err := json.Unmarshal(data, &v); err != nil {
return err
}

lang, _ := strings.CutSuffix(path.Base(p), ".json")

if lang == "en" {
if err := verifyEn(v); err != nil {
return err
}
} else if lang == "cy" {
if err := verifyCy(v); err != nil {
return err
}
} else {
return errors.New("only supports en or cy")
}

messages := Messages{
Singles: map[string]singleMessage{},
Plurals: map[string]pluralMessage{},
}

for key, message := range v {
if message.S != "" {
messages.Singles[key] = newSingleMessage(message.S)
} else {
messages.Plurals[key] = pluralMessage{
One: newSingleMessage(message.One),
Two: newSingleMessage(message.Two),
Few: newSingleMessage(message.Few),
Many: newSingleMessage(message.Many),
Other: newSingleMessage(message.Other),
}
}
}

b.messages[lang] = messages
return nil
}

func verifyEn(v map[string]parsedMessage) error {
for key, message := range v {
if message.S != "" {
continue
}

if message.One != "" && message.Other != "" && message.Two == "" && message.Few == "" && message.Many == "" {
continue
}

return fmt.Errorf("problem with key: %s", key)
}

return nil
}

func verifyCy(v map[string]parsedMessage) error {
for key, message := range v {
if message.S != "" {
continue
}

if message.One != "" && message.Other != "" && message.Two != "" && message.Few != "" && message.Many != "" {
continue
}

return fmt.Errorf("problem with key: %s", key)
}

return nil
}

func (b *Bundle) For(lang Lang) *Localizer {
return &Localizer{b.messages[lang.String()], false, lang}
}
114 changes: 114 additions & 0 deletions internal/localize/bundle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package localize

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNewBundle(t *testing.T) {
assert := assert.New(t)
bundle, err := NewBundle("testdata/en.json", "testdata/cy.json")
assert.Nil(err)

en := bundle.For(En)
assert.Equal("A", en.T("a"))
assert.Equal("key does not exist", en.T("key does not exist"))

assert.Equal("A person", en.Format("af", map[string]interface{}{"x": "person"}))
assert.Equal("key does not exist", en.Format("key does not exist", map[string]interface{}{"x": "person"}))

assert.Equal("1 ONE", en.Count("c", 1))
assert.Equal("2 OTHER", en.Count("c", 2))
assert.Equal("key does not exist", en.Count("key does not exist", 3))

assert.Equal("1 ONE FORMATTED", en.FormatCount("d", 1, map[string]interface{}{"x": "FORMATTED"}))
assert.Equal("2 OTHER FORMATTED", en.FormatCount("d", 2, map[string]interface{}{"x": "FORMATTED"}))
assert.Equal("key does not exist", en.FormatCount("key does not exist", 2, map[string]interface{}{"x": "FORMATTED"}))

cy := bundle.For(Cy)
assert.Equal("C", cy.T("a"))

assert.Equal("C person", cy.Format("af", map[string]interface{}{"x": "person"}))

assert.Equal("1 one", cy.Count("c", 1))
assert.Equal("2 two", cy.Count("c", 2))
assert.Equal("3 few", cy.Count("c", 3))
assert.Equal("4 other", cy.Count("c", 4))
assert.Equal("5 other", cy.Count("c", 5))
assert.Equal("6 many", cy.Count("c", 6))
assert.Equal("7 other", cy.Count("c", 7))

assert.Equal("1 one formatted", cy.FormatCount("d", 1, map[string]interface{}{"x": "formatted"}))
assert.Equal("2 two formatted", cy.FormatCount("d", 2, map[string]interface{}{"x": "formatted"}))
assert.Equal("3 few formatted", cy.FormatCount("d", 3, map[string]interface{}{"x": "formatted"}))
assert.Equal("4 other formatted", cy.FormatCount("d", 4, map[string]interface{}{"x": "formatted"}))
assert.Equal("5 other formatted", cy.FormatCount("d", 5, map[string]interface{}{"x": "formatted"}))
assert.Equal("6 many formatted", cy.FormatCount("d", 6, map[string]interface{}{"x": "formatted"}))
assert.Equal("7 other formatted", cy.FormatCount("d", 7, map[string]interface{}{"x": "formatted"}))
}

func TestNewBundleWhenBadFormat(t *testing.T) {
_, err := NewBundle("testdata/bad/en.json")
assert.NotNil(t, err)

_, err = NewBundle("testdata/bad/cy.json")
assert.NotNil(t, err)
}

func TestNewBundleWhenMissingFile(t *testing.T) {
_, err := NewBundle("testdata/a.json")
assert.NotNil(t, err)
}

func TestNewBundleWhenOtherLang(t *testing.T) {
_, err := NewBundle("testdata/zz.json")
assert.NotNil(t, err)
}

func TestNewBundleWhenMalformed(t *testing.T) {
_, err := NewBundle("testdata/malformed/en.json")
assert.NotNil(t, err)
}

func TestNewBundleWithTransKeys(t *testing.T) {
assert := assert.New(t)
bundle, _ := NewBundle("testdata/en.json", "testdata/cy.json")

en := bundle.For(En)
en.SetShowTranslationKeys(true)

assert.Equal("{A} [a]", en.T("a"))
assert.Equal("{key does not exist} [key does not exist]", en.T("key does not exist"))

assert.Equal("{A person} [af]", en.Format("af", map[string]interface{}{"x": "person"}))

assert.Equal("{1 ONE} [c]", en.Count("c", 1))
assert.Equal("{2 OTHER} [c]", en.Count("c", 2))

assert.Equal("{1 ONE FORMATTED} [d]", en.FormatCount("d", 1, map[string]interface{}{"x": "FORMATTED"}))
assert.Equal("{2 OTHER FORMATTED} [d]", en.FormatCount("d", 2, map[string]interface{}{"x": "FORMATTED"}))

cy := bundle.For(Cy)
cy.SetShowTranslationKeys(true)

assert.Equal("{C} [a]", cy.T("a"))

assert.Equal("{C person} [af]", cy.Format("af", map[string]interface{}{"x": "person"}))

assert.Equal("{1 one} [c]", cy.Count("c", 1))
assert.Equal("{2 two} [c]", cy.Count("c", 2))
assert.Equal("{3 few} [c]", cy.Count("c", 3))
assert.Equal("{4 other} [c]", cy.Count("c", 4))
assert.Equal("{5 other} [c]", cy.Count("c", 5))
assert.Equal("{6 many} [c]", cy.Count("c", 6))
assert.Equal("{7 other} [c]", cy.Count("c", 7))

assert.Equal("{1 one formatted} [d]", cy.FormatCount("d", 1, map[string]interface{}{"x": "formatted"}))
assert.Equal("{2 two formatted} [d]", cy.FormatCount("d", 2, map[string]interface{}{"x": "formatted"}))
assert.Equal("{3 few formatted} [d]", cy.FormatCount("d", 3, map[string]interface{}{"x": "formatted"}))
assert.Equal("{4 other formatted} [d]", cy.FormatCount("d", 4, map[string]interface{}{"x": "formatted"}))
assert.Equal("{5 other formatted} [d]", cy.FormatCount("d", 5, map[string]interface{}{"x": "formatted"}))
assert.Equal("{6 many formatted} [d]", cy.FormatCount("d", 6, map[string]interface{}{"x": "formatted"}))
assert.Equal("{7 other formatted} [d]", cy.FormatCount("d", 7, map[string]interface{}{"x": "formatted"}))
}
62 changes: 23 additions & 39 deletions internal/localize/localizer.go
Original file line number Diff line number Diff line change
@@ -1,78 +1,62 @@
package localize

import (
"encoding/json"
"fmt"
"strings"
"time"
"unicode"
"unicode/utf8"

"github.com/ministryofjustice/opg-modernising-lpa/internal/date"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)

type Bundle struct {
*i18n.Bundle
}

func NewBundle(paths ...string) Bundle {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
for _, path := range paths {
bundle.LoadMessageFile(path)
}

return Bundle{bundle}
}

func (b Bundle) For(lang Lang) *Localizer {
return &Localizer{
i18n.NewLocalizer(b.Bundle, lang.String()),
false,
lang,
}
}

type Localizer struct {
*i18n.Localizer
messages Messages
showTranslationKeys bool
Lang Lang
}

func (l Localizer) T(messageID string) string {
msg, err := l.Localize(&i18n.LocalizeConfig{MessageID: messageID})

if err != nil {
func (l *Localizer) T(messageID string) string {
msg, ok := l.messages.Find(messageID)
if !ok {
return l.translate(messageID, messageID)
}

return l.translate(msg, messageID)
return l.translate(msg.S, messageID)
}

func (l Localizer) Format(messageID string, data map[string]interface{}) string {
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, TemplateData: data}), messageID)
func (l *Localizer) Format(messageID string, data map[string]interface{}) string {
msg, ok := l.messages.Find(messageID)
if !ok {
return l.translate(messageID, messageID)
}

return l.translate(msg.Execute(data), messageID)
}

func (l Localizer) Count(messageID string, count int) string {
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, PluralCount: count}), messageID)
func (l *Localizer) Count(messageID string, count int) string {
return l.FormatCount(messageID, count, map[string]any{})
}

func (l Localizer) FormatCount(messageID string, count int, data map[string]interface{}) string {
func (l *Localizer) FormatCount(messageID string, count int, data map[string]any) string {
msg, ok := l.messages.FindPlural(messageID, count)
if !ok {
return l.translate(messageID, messageID)
}

data["PluralCount"] = count
return l.translate(l.MustLocalize(&i18n.LocalizeConfig{MessageID: messageID, PluralCount: count, TemplateData: data}), messageID)
return l.translate(msg.Execute(data), messageID)
}

func (l Localizer) translate(translation, messageID string) string {
func (l *Localizer) translate(translation, messageID string) string {
if l.showTranslationKeys {
return fmt.Sprintf("{%s} [%s]", translation, messageID)
} else {
return translation
}
}

func (l Localizer) ShowTranslationKeys() bool {
func (l *Localizer) ShowTranslationKeys() bool {
return l.showTranslationKeys
}

Expand Down
Loading

0 comments on commit 974aec4

Please sign in to comment.