diff --git a/README.md b/README.md index 7dd63152..b52d5777 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ import "github.com/nicksnyder/go-i18n/v2/i18n" Create a Bundle to use for the lifetime of your application. ```go -bundle := i18n.NewBundle(language.English) +bundle := &i18n.Bundle{DefaultLanguage: language.English} ``` Create a Localizer to use for a set of language preferences. diff --git a/v2/example/main.go b/v2/example/main.go index 838e1456..e10ff80f 100644 --- a/v2/example/main.go +++ b/v2/example/main.go @@ -27,7 +27,7 @@ var page = template.Must(template.New("").Parse(` `)) func main() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) // No need to load active.en.toml since we are providing default translations. // bundle.MustLoadMessageFile("active.en.toml") diff --git a/v2/goi18n/extract_command_test.go b/v2/goi18n/extract_command_test.go index 38184301..0347041e 100644 --- a/v2/goi18n/extract_command_test.go +++ b/v2/goi18n/extract_command_test.go @@ -50,7 +50,7 @@ func TestExtract(t *testing.T) { import "github.com/nicksnyder/go-i18n/v2/i18n" func main() { - bundle := i18n.NewBundle() + bundle := &i18n.Bundle{} l := i18n.NewLocalizer(bundle, "en") l.Localize(&i18n.LocalizeConfig{MessageID: "Plural ID"}) } @@ -69,7 +69,7 @@ func TestExtract(t *testing.T) { import "github.com/nicksnyder/go-i18n/v2/i18n" func main() { - bundle := i18n.NewBundle() + bundle := &i18n.Bundle{} l := i18n.NewLocalizer(bundle, "en") l.MustLocalize(&i18n.LocalizeConfig{MessageID: "Plural ID"}) } diff --git a/v2/i18n/bundle.go b/v2/i18n/bundle.go index 4bb89e1c..2a8faaf2 100644 --- a/v2/i18n/bundle.go +++ b/v2/i18n/bundle.go @@ -1,7 +1,6 @@ package i18n import ( - "encoding/json" "fmt" "io/ioutil" @@ -18,34 +17,31 @@ type UnmarshalFunc = internal.UnmarshalFunc // Most applications only need a single bundle // that is initialized early in the application's lifecycle. type Bundle struct { + // DefaultLanguage is the default language of the bundle. + DefaultLanguage language.Tag + + // UnmarshalFuncs is a map of file extensions to UnmarshalFuncs. + UnmarshalFuncs map[string]UnmarshalFunc + messageTemplates map[language.Tag]map[string]*internal.MessageTemplate pluralRules plural.Rules - unmarshalFuncs map[string]UnmarshalFunc - defaultTag language.Tag tags []language.Tag matcher language.Matcher } -// NewBundle returns a new bundle that contains the -// CLDR plural rules and a json unmarshaler. -func NewBundle(defaultTag language.Tag) *Bundle { - b := &Bundle{ - defaultTag: defaultTag, - pluralRules: plural.DefaultRules(), - unmarshalFuncs: map[string]UnmarshalFunc{ - "json": json.Unmarshal, - }, +func (b *Bundle) init() { + if b.pluralRules == nil { + b.pluralRules = plural.DefaultRules() } - b.addTag(defaultTag) - return b + b.addTag(b.DefaultLanguage) } // RegisterUnmarshalFunc registers an UnmarshalFunc for format. func (b *Bundle) RegisterUnmarshalFunc(format string, unmarshalFunc UnmarshalFunc) { - if b.unmarshalFuncs == nil { - b.unmarshalFuncs = make(map[string]UnmarshalFunc) + if b.UnmarshalFuncs == nil { + b.UnmarshalFuncs = make(map[string]UnmarshalFunc) } - b.unmarshalFuncs[format] = unmarshalFunc + b.UnmarshalFuncs[format] = unmarshalFunc } // LoadMessageFile loads the bytes from path @@ -75,7 +71,7 @@ type MessageFile = internal.MessageFile // // The language tag of the file is everything after the second to last "." or after the last path separator, but before the format. func (b *Bundle) ParseMessageFileBytes(buf []byte, path string) (*MessageFile, error) { - messageFile, err := internal.ParseMessageFileBytes(buf, path, b.unmarshalFuncs) + messageFile, err := internal.ParseMessageFileBytes(buf, path, b.UnmarshalFuncs) if err != nil { return nil, err } @@ -96,9 +92,7 @@ func (b *Bundle) MustParseMessageFileBytes(buf []byte, path string) { // AddMessages adds messages for a language. // It is useful if your messages are in a format not supported by ParseMessageFileBytes. func (b *Bundle) AddMessages(tag language.Tag, messages ...*Message) error { - if b.pluralRules == nil { - b.pluralRules = plural.DefaultRules() - } + b.init() pluralRule := b.pluralRules.Rule(tag) if pluralRule == nil { return fmt.Errorf("no plural rule registered for %s", tag) diff --git a/v2/i18n/bundle_test.go b/v2/i18n/bundle_test.go index 037a17c5..43f40202 100644 --- a/v2/i18n/bundle_test.go +++ b/v2/i18n/bundle_test.go @@ -1,7 +1,6 @@ package i18n import ( - "encoding/json" "reflect" "testing" @@ -34,7 +33,7 @@ var everythingMessage = internal.MustNewMessage(map[string]string{ }) func TestPseudoLanguage(t *testing.T) { - bundle := NewBundle(language.English) + bundle := &Bundle{DefaultLanguage: language.English} bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) expected := "simple simple" bundle.MustParseMessageFileBytes([]byte(` @@ -52,7 +51,7 @@ simple = "simple simple" } func TestPseudoLanguagePlural(t *testing.T) { - bundle := NewBundle(language.English) + bundle := &Bundle{DefaultLanguage: language.English} bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.MustParseMessageFileBytes([]byte(` [everything] @@ -82,7 +81,6 @@ zero = "zero translation" func TestJSON(t *testing.T) { var bundle Bundle - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) bundle.MustParseMessageFileBytes([]byte(`{ "simple": "simple translation", "detail": { @@ -163,7 +161,6 @@ other = "other translation" func TestV1Format(t *testing.T) { var bundle Bundle - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) bundle.MustParseMessageFileBytes([]byte(`[ { "id": "simple", @@ -191,7 +188,6 @@ func TestV1Format(t *testing.T) { func TestV1FlatFormat(t *testing.T) { var bundle Bundle - bundle.RegisterUnmarshalFunc("json", json.Unmarshal) bundle.MustParseMessageFileBytes([]byte(`{ "simple": { "other": "simple translation" diff --git a/v2/i18n/doc.go b/v2/i18n/doc.go index 5b79a113..7b56a717 100644 --- a/v2/i18n/doc.go +++ b/v2/i18n/doc.go @@ -2,7 +2,7 @@ // according to a set of locale preferences. // // Create a Bundle to use for the lifetime of your application. -// bundle := i18n.NewBundle(language.English) +// bundle := &i18n.Bundle{DefaultLanguage: language.English} // // Create a Localizer to use for a set of language preferences. // func(w http.ResponseWriter, r *http.Request) { diff --git a/v2/i18n/example_test.go b/v2/i18n/example_test.go index 2256e636..13fd6413 100644 --- a/v2/i18n/example_test.go +++ b/v2/i18n/example_test.go @@ -9,7 +9,7 @@ import ( ) func ExampleLocalizer_MustLocalize() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} localizer := i18n.NewLocalizer(bundle, "en") fmt.Println(localizer.MustLocalize(&i18n.LocalizeConfig{ DefaultMessage: &i18n.Message{ @@ -22,7 +22,7 @@ func ExampleLocalizer_MustLocalize() { } func ExampleLocalizer_MustLocalize_noDefaultMessage() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.MustParseMessageFileBytes([]byte(` HelloWorld = "Hello World!" @@ -45,7 +45,7 @@ HelloWorld = "Hola Mundo!" } func ExampleLocalizer_MustLocalize_plural() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} localizer := i18n.NewLocalizer(bundle, "en") catsMessage := &i18n.Message{ ID: "Cats", @@ -71,7 +71,7 @@ func ExampleLocalizer_MustLocalize_plural() { } func ExampleLocalizer_MustLocalize_template() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} localizer := i18n.NewLocalizer(bundle, "en") helloPersonMessage := &i18n.Message{ ID: "HelloPerson", @@ -86,7 +86,7 @@ func ExampleLocalizer_MustLocalize_template() { } func ExampleLocalizer_MustLocalize_plural_template() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} localizer := i18n.NewLocalizer(bundle, "en") personCatsMessage := &i18n.Message{ ID: "PersonCats", @@ -124,7 +124,7 @@ func ExampleLocalizer_MustLocalize_plural_template() { } func ExampleLocalizer_MustLocalize_customTemplateDelims() { - bundle := i18n.NewBundle(language.English) + bundle := &i18n.Bundle{DefaultLanguage: language.English} localizer := i18n.NewLocalizer(bundle, "en") helloPersonMessage := &i18n.Message{ ID: "HelloPerson", diff --git a/v2/i18n/localizer.go b/v2/i18n/localizer.go index 1e5dc00d..f9d516fa 100644 --- a/v2/i18n/localizer.go +++ b/v2/i18n/localizer.go @@ -24,6 +24,7 @@ type Localizer struct { // in the bundle according to the language preferences in langs. // It can parse Accept-Language headers as defined in http://www.ietf.org/rfc/rfc2616.txt. func NewLocalizer(bundle *Bundle, langs ...string) *Localizer { + bundle.init() return &Localizer{ bundle: bundle, tags: parseTags(langs), @@ -129,7 +130,7 @@ func (l *Localizer) getTemplate(id string, defaultMessage *Message) (language.Ta if template != nil { return fastTag, template } - if fastTag == l.bundle.defaultTag { + if fastTag == l.bundle.DefaultLanguage { if defaultMessage == nil { return fastTag, nil } @@ -141,8 +142,8 @@ func (l *Localizer) getTemplate(id string, defaultMessage *Message) (language.Ta // so we need to create a new matcher that contains only the tags in the bundle // that have this message. foundTags := make([]language.Tag, 0, len(l.bundle.messageTemplates)) - if l.bundle.defaultTag != fastTag { - foundTags = append(foundTags, l.bundle.defaultTag) + if l.bundle.DefaultLanguage != fastTag { + foundTags = append(foundTags, l.bundle.DefaultLanguage) } for t, templates := range l.bundle.messageTemplates { if t == fastTag { @@ -161,9 +162,9 @@ func (l *Localizer) getTemplate(id string, defaultMessage *Message) (language.Ta } } if defaultMessage == nil { - return l.bundle.defaultTag, nil + return l.bundle.DefaultLanguage, nil } - return l.bundle.defaultTag, internal.NewMessageTemplate(defaultMessage) + return l.bundle.DefaultLanguage, internal.NewMessageTemplate(defaultMessage) } func (l *Localizer) matchTemplate(id string, matcher language.Matcher, tags []language.Tag) (language.Tag, *internal.MessageTemplate) { diff --git a/v2/i18n/localizer_test.go b/v2/i18n/localizer_test.go index be15afd5..9df62f58 100644 --- a/v2/i18n/localizer_test.go +++ b/v2/i18n/localizer_test.go @@ -476,7 +476,7 @@ func TestLocalizer_Localize(t *testing.T) { } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - bundle := NewBundle(testCase.defaultLanguage) + bundle := &Bundle{DefaultLanguage: testCase.defaultLanguage} for tag, messages := range testCase.messages { bundle.AddMessages(tag, messages...) } diff --git a/v2/internal/parse.go b/v2/internal/parse.go index 171a1e30..a5cee87c 100644 --- a/v2/internal/parse.go +++ b/v2/internal/parse.go @@ -1,6 +1,7 @@ package internal import ( + "encoding/json" "fmt" "os" @@ -30,12 +31,13 @@ func ParseMessageFileBytes(buf []byte, path string, unmarshalFuncs map[string]Un if len(buf) == 0 { return messageFile, nil } - var unmarshalFunc UnmarshalFunc - if unmarshalFuncs != nil { - unmarshalFunc = unmarshalFuncs[messageFile.Format] - } + unmarshalFunc := unmarshalFuncs[messageFile.Format] if unmarshalFunc == nil { - return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format) + if messageFile.Format == "json" { + unmarshalFunc = json.Unmarshal + } else { + return nil, fmt.Errorf("no unmarshaler registered for %s", messageFile.Format) + } } var raw interface{} if err := unmarshalFunc(buf, &raw); err != nil { diff --git a/v2/internal/parse_test.go b/v2/internal/parse_test.go new file mode 100644 index 00000000..dfe65a3b --- /dev/null +++ b/v2/internal/parse_test.go @@ -0,0 +1,50 @@ +package internal + +import ( + "reflect" + "testing" + + "golang.org/x/text/language" +) + +func TestParseMessageFileBytes(t *testing.T) { + testCases := []struct { + file string + path string + unmarshalFuncs map[string]UnmarshalFunc + messageFile *MessageFile + err error + }{ + { + file: `{"hello": "world"}`, + path: "en.json", + messageFile: &MessageFile{ + Path: "en.json", + Tag: language.English, + Format: "json", + Messages: []*Message{{ + ID: "hello", + Other: "world", + }}, + }, + }, + } + for _, testCase := range testCases { + actual, err := ParseMessageFileBytes([]byte(testCase.file), testCase.path, testCase.unmarshalFuncs) + if err != testCase.err { + t.Fatalf("expected error %#v; got %#v", testCase.err, err) + } + if actual.Path != testCase.messageFile.Path { + t.Fatalf("expected path %q; got %q", testCase.messageFile.Path, actual.Path) + } + if actual.Tag != testCase.messageFile.Tag { + t.Fatalf("expected tag %q; got %q", testCase.messageFile.Tag, actual.Tag) + } + if actual.Format != testCase.messageFile.Format { + t.Fatalf("expected format %q; got %q", testCase.messageFile.Format, actual.Format) + } + if !reflect.DeepEqual(actual.Messages, testCase.messageFile.Messages) { + t.Fatalf("expected %#v; got %#v", testCase.messageFile.Messages, actual.Messages) + } + } +}