diff --git a/.github/README.zh-Hans.md b/.github/README.zh-Hans.md index a7f2c4bc..8d458948 100644 --- a/.github/README.zh-Hans.md +++ b/.github/README.zh-Hans.md @@ -1,4 +1,5 @@ -# go-i18n ![Build status](https://github.com/nicksnyder/go-i18n/workflows/Build/badge.svg) [![Report card](https://goreportcard.com/badge/github.com/nicksnyder/go-i18n)](https://goreportcard.com/report/github.com/nicksnyder/go-i18n) [![codecov](https://codecov.io/gh/nicksnyder/go-i18n/branch/master/graph/badge.svg)](https://codecov.io/gh/nicksnyder/go-i18n) [![Sourcegraph](https://sourcegraph.com/github.com/nicksnyder/go-i18n/-/badge.svg)](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge) +# go-i18n +![Build status](https://github.com/nicksnyder/go-i18n/workflows/Build/badge.svg) [![Report card](https://goreportcard.com/badge/github.com/nicksnyder/go-i18n/v2)](https://goreportcard.com/report/github.com/nicksnyder/go-i18n/v2) [![codecov](https://codecov.io/gh/nicksnyder/go-i18n/graph/badge.svg?token=A9aMfR9vxG)](https://codecov.io/gh/nicksnyder/go-i18n) [![Sourcegraph](https://sourcegraph.com/github.com/nicksnyder/go-i18n/-/badge.svg)](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge) go-i18n 是一个帮助您将 Go 程序翻译成多种语言的 Go [包](#package-i18n)和[命令](#command-goi18n)。 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..768bcda1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2d3279e1..da6035e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,39 +1,44 @@ name: Build on: - - push - - pull_request + push: + branches: + - main + pull_request: + jobs: build: name: Build runs-on: ubuntu-latest - if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'pull_request' steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: stable - name: Git checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean --snapshot - name: Test run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... - name: Upload coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true build_1_18: name: Build with Go 1.18 runs-on: ubuntu-latest if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'pull_request' steps: - name: Install Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version: '1.18' - name: Git checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build and test run: | go get ./... diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 3c0f0a68..bb05c462 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -13,12 +13,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '1.21' - cache: false + go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.55 + version: v1.61 diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 71fc82fe..7853fdc4 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -11,17 +11,17 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: '^1.19.3' + go-version: stable - name: Release - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 with: version: latest args: release --clean diff --git a/.github/workflows/lsif-go.yml b/.github/workflows/lsif-go.yml deleted file mode 100644 index d5d8e107..00000000 --- a/.github/workflows/lsif-go.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Sourcegraph code intelligence -on: - - push - -jobs: - lsif-go: - runs-on: ubuntu-latest - container: sourcegraph/lsif-go:latest - steps: - - uses: actions/checkout@v1 - - name: Generate LSIF data - run: lsif-go - - name: Upload LSIF data to Sourcegraph.com - run: src lsif upload -github-token=${{ secrets.GITHUB_TOKEN }} -ignore-upload-failure - diff --git a/.github/workflows/scip-go.yml b/.github/workflows/scip-go.yml new file mode 100644 index 00000000..4c444d03 --- /dev/null +++ b/.github/workflows/scip-go.yml @@ -0,0 +1,23 @@ +name: Sourcegraph code intelligence +on: + - push + +jobs: + scip-go: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install scip-go + run: | + curl -L https://github.com/sourcegraph/scip-go/releases/download/v0.1.21/scip-go_0.1.21_linux_amd64.tar.gz -o scip-go.tar.gz --no-progress-meter + tar -xf scip-go.tar.gz + chmod +x ./scip-go + - name: Install src + run: | + curl -L https://sourcegraph.com/.api/src-cli/src_linux_amd64 -o src --no-progress-meter + chmod +x ./src + - name: Generate SCIP data + run: ./scip-go + - name: Upload SCIP data to Sourcegraph.com + run: SRC_ACCESS_TOKEN=${{ secrets.SRC_ACCESS_TOKEN }} ./src code-intel upload -github-token=${{ secrets.GITHUB_TOKEN }} -no-progress + diff --git a/.goreleaser.yml b/.goreleaser.yml index d9352adb..a2d3b837 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,3 +1,4 @@ +version: 2 builds: - binary: goi18n main: ./goi18n/ diff --git a/README.md b/README.md index c6e09f95..0b826e31 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# go-i18n ![Build status](https://github.com/nicksnyder/go-i18n/workflows/Build/badge.svg) [![Report card](https://goreportcard.com/badge/github.com/nicksnyder/go-i18n)](https://goreportcard.com/report/github.com/nicksnyder/go-i18n) [![codecov](https://codecov.io/gh/nicksnyder/go-i18n/graph/badge.svg?token=A9aMfR9vxG)](https://codecov.io/gh/nicksnyder/go-i18n) [![Sourcegraph](https://sourcegraph.com/github.com/nicksnyder/go-i18n/-/badge.svg)](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge) +# go-i18n +![Build status](https://github.com/nicksnyder/go-i18n/workflows/Build/badge.svg) [![Report card](https://goreportcard.com/badge/github.com/nicksnyder/go-i18n/v2)](https://goreportcard.com/report/github.com/nicksnyder/go-i18n/v2) [![codecov](https://codecov.io/gh/nicksnyder/go-i18n/graph/badge.svg?token=A9aMfR9vxG)](https://codecov.io/gh/nicksnyder/go-i18n) [![Sourcegraph](https://sourcegraph.com/github.com/nicksnyder/go-i18n/-/badge.svg)](https://sourcegraph.com/github.com/nicksnyder/go-i18n?badge) go-i18n is a Go [package](#package-i18n) and a [command](#command-goi18n) that helps you translate Go programs into multiple languages. @@ -7,14 +8,6 @@ go-i18n is a Go [package](#package-i18n) and a [command](#command-goi18n) that h - Supports strings with named variables using [text/template](http://golang.org/pkg/text/template/) syntax. - Supports message files of any format (e.g. JSON, TOML, YAML). - - - -[**English**](README.md) · [**简体中文**](.github/README.zh-Hans.md) - - - - ## Package i18n [![Go Reference](https://pkg.go.dev/badge/github.com/nicksnyder/go-i18n/v2/i18n.svg)](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2/i18n) @@ -140,6 +133,13 @@ If you have added new messages to your program: - Look at the [code examples](https://github.com/nicksnyder/go-i18n/blob/main/i18n/example_test.go) and [tests](https://github.com/nicksnyder/go-i18n/blob/main/i18n/localizer_test.go). - Look at an example [application](https://github.com/nicksnyder/go-i18n/tree/main/example). +## Translations of this document + +Community translations of this document may be found in the [.github](.github) folder. + +These translations are maintained by the community, and are not maintained by the author of this project. +They are not guaranteed to be accurate or up-to-date. + ## License go-i18n is available under the MIT license. See the [LICENSE](LICENSE) file for more info. diff --git a/go.mod b/go.mod index cf273ac7..6e0fba10 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.18 require ( github.com/BurntSushi/toml v1.4.0 - golang.org/x/text v0.17.0 + golang.org/x/text v0.19.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index e61e691b..c949e0dd 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/i18n/bundle_test.go b/i18n/bundle_test.go index 917a7d3f..99706623 100644 --- a/i18n/bundle_test.go +++ b/i18n/bundle_test.go @@ -131,7 +131,7 @@ simple: simple translation # Comment detail: - description: detail description + description: detail description other: detail translation # Comment @@ -150,6 +150,47 @@ everything: expectMessage(t, bundle, language.AmericanEnglish, "everything", everythingMessage) } +func TestInvalidYAML(t *testing.T) { + bundle := NewBundle(language.English) + bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) + _, err := bundle.ParseMessageFileBytes([]byte(` +# Comment +simple: simple translation + +# Comment +detail: + description: detail description + other: detail translation + +# Comment +everything: + description: everything description + zero: zero translation + one: one translation + two: two translation + few: few translation + many: many translation + other: other translation + garbage: something + +description: translation +`), "en-US.yaml") + + expectedErr := &mixedKeysError{ + reservedKeys: []string{"description"}, + unreservedKeys: []string{"detail", "everything", "simple"}, + } + if err == nil { + t.Fatalf("expected error %#v; got nil", expectedErr) + } + if err.Error() != expectedErr.Error() { + t.Fatalf("expected error %q; got %q", expectedErr, err) + } + if c := len(bundle.messageTemplates); c > 0 { + t.Fatalf("expected no message templates in bundle; got %d", c) + } +} + func TestTOML(t *testing.T) { bundle := NewBundle(language.English) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) diff --git a/i18n/localizer_test.go b/i18n/localizer_test.go index f1c80524..5856ec6b 100644 --- a/i18n/localizer_test.go +++ b/i18n/localizer_test.go @@ -1,9 +1,11 @@ package i18n import ( + "errors" "fmt" "reflect" "testing" + gotmpl "text/template" "github.com/nicksnyder/go-i18n/v2/i18n/template" "github.com/nicksnyder/go-i18n/v2/internal/plural" @@ -648,6 +650,34 @@ func localizerTests() []localizerTest { }, expectedErr: &MessageNotFoundErr{Tag: language.English, MessageID: "Hello"}, }, + { + name: "use option missingkey=error with missing key", + defaultLanguage: language.English, + messages: map[language.Tag][]*Message{ + language.English: {{ID: "Foo", Other: "Foo {{.bar}}"}}, + }, + acceptLangs: []string{"en"}, + conf: &LocalizeConfig{ + MessageID: "Foo", + TemplateData: map[string]string{}, + TemplateParser: &template.TextParser{Option: "missingkey=error"}, + }, + expectedErr: gotmpl.ExecError{Name: "", Err: errors.New(`template: :1:6: executing "" at <.bar>: map has no entry for key "bar"`)}, + }, + { + name: "use option missingkey=default with missing key", + defaultLanguage: language.English, + messages: map[language.Tag][]*Message{ + language.English: {{ID: "Foo", Other: "Foo {{.bar}}"}}, + }, + acceptLangs: []string{"en"}, + conf: &LocalizeConfig{ + MessageID: "Foo", + TemplateData: map[string]string{}, + TemplateParser: &template.TextParser{Option: "missingkey=default"}, + }, + expectedLocalized: "Foo ", + }, } } @@ -663,7 +693,7 @@ func TestLocalizer_Localize(t *testing.T) { check := func(localized string, err error) { t.Helper() if !reflect.DeepEqual(err, test.expectedErr) { - t.Errorf("expected error %#v; got %#v", test.expectedErr, err) + t.Errorf("\nexpected error: %#v\n got error: %#v", test.expectedErr, err) } if localized != test.expectedLocalized { t.Errorf("expected localized string %q; got %q", test.expectedLocalized, localized) diff --git a/i18n/message.go b/i18n/message.go index 99136a2a..30587693 100644 --- a/i18n/message.go +++ b/i18n/message.go @@ -2,6 +2,7 @@ package i18n import ( "fmt" + "sort" "strings" ) @@ -175,47 +176,111 @@ func stringSubmap(k string, v interface{}, strdata map[string]string) error { } } -// isMessage tells whether the given data is a message, or a map containing -// nested messages. -// A map is assumed to be a message if it contains any of the "reserved" keys: -// "id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other" -// with a string value. -// e.g., +var reservedKeys = map[string]struct{}{ + "id": {}, + "description": {}, + "hash": {}, + "leftdelim": {}, + "rightdelim": {}, + "zero": {}, + "one": {}, + "two": {}, + "few": {}, + "many": {}, + "other": {}, + "translation": {}, +} + +func isReserved(key string, val any) bool { + if _, ok := reservedKeys[key]; ok { + if key == "translation" { + return true + } + if _, ok := val.(string); ok { + return true + } + } + return false +} + +// isMessage returns true if v contains only message keys and false if it contains no message keys. +// It returns an error if v contains both message and non-message keys. // - {"message": {"description": "world"}} is a message -// - {"message": {"description": "world", "foo": "bar"}} is a message ("foo" key is ignored) -// - {"notmessage": {"description": {"hello": "world"}}} is not -// - {"notmessage": {"foo": "bar"}} is not -func isMessage(v interface{}) bool { - reservedKeys := []string{"id", "description", "hash", "leftdelim", "rightdelim", "zero", "one", "two", "few", "many", "other"} +// - {"error": {"description": "world", "foo": "bar"}} is an error +// - {"notmessage": {"description": {"hello": "world"}}} is not a message +// - {"notmessage": {"foo": "bar"}} is not a message +func isMessage(v interface{}) (bool, error) { switch data := v.(type) { case nil, string: - return true + return true, nil case map[string]interface{}: - for _, key := range reservedKeys { + reservedKeyCount := 0 + for key := range reservedKeys { val, ok := data[key] - if !ok { - continue + if ok && isReserved(key, val) { + reservedKeyCount++ } - _, ok = val.(string) - if !ok { - continue + } + if reservedKeyCount == 0 { + return false, nil + } + if len(data) > reservedKeyCount { + reservedKeys := make([]string, 0, reservedKeyCount) + unreservedKeys := make([]string, 0, len(data)-reservedKeyCount) + for k, v := range data { + if isReserved(k, v) { + reservedKeys = append(reservedKeys, k) + } else { + unreservedKeys = append(unreservedKeys, k) + } + } + return false, &mixedKeysError{ + reservedKeys: reservedKeys, + unreservedKeys: unreservedKeys, } - // v is a message if it contains a "reserved" key holding a string value - return true } + return true, nil case map[interface{}]interface{}: - for _, key := range reservedKeys { + reservedKeyCount := 0 + for key := range reservedKeys { val, ok := data[key] - if !ok { - continue + if ok && isReserved(key, val) { + reservedKeyCount++ } - _, ok = val.(string) - if !ok { - continue + } + if reservedKeyCount == 0 { + return false, nil + } + if len(data) > reservedKeyCount { + reservedKeys := make([]string, 0, reservedKeyCount) + unreservedKeys := make([]string, 0, len(data)-reservedKeyCount) + for key, v := range data { + k, ok := key.(string) + if !ok { + unreservedKeys = append(unreservedKeys, fmt.Sprintf("%+v", key)) + } else if isReserved(k, v) { + reservedKeys = append(reservedKeys, k) + } else { + unreservedKeys = append(unreservedKeys, k) + } + } + return false, &mixedKeysError{ + reservedKeys: reservedKeys, + unreservedKeys: unreservedKeys, } - // v is a message if it contains a "reserved" key holding a string value - return true } + return true, nil } - return false + return false, nil +} + +type mixedKeysError struct { + reservedKeys []string + unreservedKeys []string +} + +func (e *mixedKeysError) Error() string { + sort.Strings(e.reservedKeys) + sort.Strings(e.unreservedKeys) + return fmt.Sprintf("reserved keys %v mixed with unreserved keys %v", e.reservedKeys, e.unreservedKeys) } diff --git a/i18n/parse.go b/i18n/parse.go index 103d7bfa..fc7f7078 100644 --- a/i18n/parse.go +++ b/i18n/parse.go @@ -43,7 +43,12 @@ func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]Un return nil, err } - if messageFile.Messages, err = recGetMessages(raw, isMessage(raw), true); err != nil { + m, err := isMessage(raw) + if err != nil { + return nil, err + } + + if messageFile.Messages, err = recGetMessages(raw, m, true); err != nil { return nil, err } @@ -105,7 +110,11 @@ func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Messa messages = make([]*Message, 0, len(data)) for _, data := range data { // recursively scan slice items - childMessages, err := recGetMessages(data, isMessage(data), false) + m, err := isMessage(data) + if err != nil { + return nil, err + } + childMessages, err := recGetMessages(data, m, false) if err != nil { return nil, err } @@ -127,7 +136,10 @@ func recGetMessages(raw interface{}, isMapMessage, isInitialCall bool) ([]*Messa } func addChildMessages(id string, data interface{}, messages []*Message) ([]*Message, error) { - isChildMessage := isMessage(data) + isChildMessage, err := isMessage(data) + if err != nil { + return nil, err + } childMessages, err := recGetMessages(data, isChildMessage, false) if err != nil { return nil, err diff --git a/i18n/parse_test.go b/i18n/parse_test.go index d0668170..29f05f4d 100644 --- a/i18n/parse_test.go +++ b/i18n/parse_test.go @@ -1,6 +1,7 @@ package i18n import ( + "errors" "reflect" "sort" "testing" @@ -32,6 +33,29 @@ func TestParseMessageFileBytes(t *testing.T) { }}, }, }, + { + name: "nested with reserved key", + file: `{"nested": {"description": {"other": "world"}}}`, + path: "en.json", + messageFile: &MessageFile{ + Path: "en.json", + Tag: language.English, + Format: "json", + Messages: []*Message{{ + ID: "nested.description", + Other: "world", + }}, + }, + }, + { + name: "basic test reserved key top level", + file: `{"other": "world", "foo": "bar"}`, + path: "en.json", + err: &mixedKeysError{ + reservedKeys: []string{"other"}, + unreservedKeys: []string{"foo"}, + }, + }, { name: "basic test with dot separator in key", file: `{"prepended.hello": "world"}`, @@ -97,14 +121,9 @@ func TestParseMessageFileBytes(t *testing.T) { name: "basic test with description and dummy", file: `{"notnested": {"description": "world", "dummy": "nothing"}}`, path: "en.json", - messageFile: &MessageFile{ - Path: "en.json", - Tag: language.English, - Format: "json", - Messages: []*Message{{ - ID: "notnested", - Description: "world", - }}, + err: &mixedKeysError{ + reservedKeys: []string{"description"}, + unreservedKeys: []string{"dummy"}, }, }, { @@ -187,6 +206,29 @@ some-keys: }, }, }, + { + name: "YAML number key test", + file: ` +some-keys: + hello: world + 2: legit`, + path: "en.yaml", + unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal}, + err: errors.New("expected key to be string but got 2"), + }, + { + name: "YAML extra number key test", + file: ` +some-keys: + other: world + 2: legit`, + path: "en.yaml", + unmarshalFuncs: map[string]UnmarshalFunc{"yaml": yaml.Unmarshal}, + err: &mixedKeysError{ + reservedKeys: []string{"other"}, + unreservedKeys: []string{"2"}, + }, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -210,13 +252,21 @@ some-keys: t.Errorf("expected format %q; got %q", testCase.messageFile.Format, actual.Format) } if !equalMessages(actual.Messages, testCase.messageFile.Messages) { - t.Errorf("expected %#v; got %#v", testCase.messageFile.Messages, actual.Messages) + t.Errorf("expected %#v; got %#v", deref(testCase.messageFile.Messages), deref(actual.Messages)) } } }) } } +func deref(mptrs []*Message) []Message { + messages := make([]Message, len(mptrs)) + for i, m := range mptrs { + messages[i] = *m + } + return messages +} + // equalMessages compares two slices of messages, ignoring private fields and order. // Sorts both input slices, which are therefore modified by this function. func equalMessages(m1, m2 []*Message) bool { diff --git a/i18n/template/text_parser.go b/i18n/template/text_parser.go index 76b4ba2d..e1a53858 100644 --- a/i18n/template/text_parser.go +++ b/i18n/template/text_parser.go @@ -37,7 +37,12 @@ func (te *TextParser) Parse(src, leftDelim, rightDelim string) (ParsedTemplate, rightDelim = "}}" } - tmpl, err := template.New("").Delims(leftDelim, rightDelim).Funcs(te.Funcs).Parse(src) + option := "missingkey=default" + if te.Option != "" { + option = te.Option + } + + tmpl, err := template.New("").Delims(leftDelim, rightDelim).Option(option).Funcs(te.Funcs).Parse(src) if err != nil { return nil, err }