From 893728477258ffbea8dcc364c969b3d199d57ca4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 16 Sep 2024 17:11:37 -0300 Subject: [PATCH] Fix escaping --- README.md | 15 ++++++++++++++ message/build.go | 4 ++++ mf/translator_test.go | 48 +++++++++++++++++++++++++++++++++++++++++++ parse/parser.go | 11 +++++++--- parse/parser_test.go | 26 +++++++++++++++++++++++ 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 parse/parser_test.go diff --git a/README.md b/README.md index 2a6f67e..bb46d0d 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,21 @@ tr.Trans( ) ``` +### Escaping + +Sometimes you need to print `{`, `'`, or `#`. You could escape them with `'` char. + +```yaml +# translations/messages.en.yaml +escape: "'{foo} is ''{foo}''" +``` + +```go +tr.Trans("escape", mf.Arg("foo", "bar")) +// {foo} is 'bar' +``` + + ## MessageFormat overview ### Placeholders diff --git a/message/build.go b/message/build.go index d2144f3..93c41e3 100644 --- a/message/build.go +++ b/message/build.go @@ -32,6 +32,10 @@ func Build(in parse.Message, lang language.Tag) (Evalable, error) { } func buildFragment(f parse.Fragment, lang language.Tag) (Evalable, error) { + if len(f.Escaped) > 0 { + return Content(f.Escaped[1:]), nil + } + if len(f.Text) > 0 { return Content(f.Text), nil } diff --git a/mf/translator_test.go b/mf/translator_test.go index c535a61..a7c4c53 100644 --- a/mf/translator_test.go +++ b/mf/translator_test.go @@ -418,6 +418,54 @@ func Test_translator_Trans(t *testing.T) { "First step on the Moon on July 21, 1969.", false, }, + { + "escaping quotes not required", + "foo ' {foo}.", + language.English, + []TranslationArg{Arg("foo", "bar")}, + "foo ' bar.", + false, + }, + { + "escaping single curly brace", + "foo '{ {foo}.", + language.English, + []TranslationArg{Arg("foo", "bar")}, + "foo { bar.", + false, + }, + { + "escaping single quote", + "foo '' {foo}.", + language.English, + []TranslationArg{Arg("foo", "bar")}, + "foo ' bar.", + false, + }, + { + "escaping single quote before curly brace", + "foo ''{foo}''.", + language.English, + []TranslationArg{Arg("foo", "bar")}, + "foo 'bar'.", + false, + }, + { + "escaping", + "foo '{ ''{foo} {num, plural, one {''#'' '# ' '{ one}, other {other}}.", + language.English, + []TranslationArg{Arg("foo", "bar"), Arg("num", 1)}, + "foo { 'bar '1' # ' { one.", + false, + }, + { + "escaping", + "'{foo} is ''{foo}''", + language.English, + []TranslationArg{Arg("foo", "bar")}, + "{foo} is 'bar'", + false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/parse/parser.go b/parse/parser.go index 3ec56a8..6394e84 100644 --- a/parse/parser.go +++ b/parse/parser.go @@ -32,7 +32,8 @@ type Message struct { } type Fragment struct { - Text string `(@String | @SubMessageString)` + Escaped string `(@Escaped | @SubEscaped)` + Text string `| (@String | @SubMessageString | @Quote | @SubQuote)` PlainArg *PlainArg `| @@` Func *Func `| @@` Expr *Expr `| @@` @@ -42,7 +43,9 @@ type Fragment struct { func NewParser() *participle.Parser[Message] { def := lexer.MustStateful(lexer.Rules{ "Root": { - {Name: `String`, Pattern: `[^{]+`, Action: nil}, + {Name: `Escaped`, Pattern: `'{|''`, Action: nil}, + {Name: `Quote`, Pattern: `'`, Action: nil}, + {Name: `String`, Pattern: `[^'{]+`, Action: nil}, {Name: `Expr`, Pattern: `{`, Action: lexer.Push("Expr")}, }, "Expr": { @@ -55,8 +58,10 @@ func NewParser() *participle.Parser[Message] { {Name: `SubMessage`, Pattern: `{`, Action: lexer.Push("SubMessage")}, }, "SubMessage": { - {Name: `SubMessageString`, Pattern: `[^{^}^#]+`, Action: nil}, + {Name: `SubEscaped`, Pattern: `'{|''|'#|'}`, Action: nil}, + {Name: `SubQuote`, Pattern: `'`, Action: nil}, {Name: `Octothorpe`, Pattern: `#`, Action: nil}, + {Name: `SubMessageString`, Pattern: `[^{^}^#^']+`, Action: nil}, {Name: `Expr`, Pattern: `{`, Action: lexer.Push("Expr")}, {Name: `SubMessageEnd`, Pattern: `}`, Action: lexer.Pop()}, }, diff --git a/parse/parser_test.go b/parse/parser_test.go new file mode 100644 index 0000000..dd0eb60 --- /dev/null +++ b/parse/parser_test.go @@ -0,0 +1,26 @@ +package parse + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParser_Escaping(t *testing.T) { + parser := NewParser() + + msg, err := parser.Parse("", strings.NewReader("foo '{ '' ' foo")) + + require.NoError(t, err) + assert.Len(t, msg.Fragments, 7) + + assert.Equal(t, &Fragment{Text: "foo "}, msg.Fragments[0]) + assert.Equal(t, &Fragment{Escaped: "'{"}, msg.Fragments[1]) + assert.Equal(t, &Fragment{Text: " "}, msg.Fragments[2]) + assert.Equal(t, &Fragment{Escaped: "''"}, msg.Fragments[3]) + assert.Equal(t, &Fragment{Text: " "}, msg.Fragments[4]) + assert.Equal(t, &Fragment{Text: "'"}, msg.Fragments[5]) + assert.Equal(t, &Fragment{Text: " foo"}, msg.Fragments[6]) +}