├── test ├── doc.go └── utils.go ├── locale ├── json_loader.go ├── yaml_loader.go ├── loader.go ├── doc.go ├── yaml_loader_test.go └── json_loader_test.go ├── .travis.yml ├── http ├── http.go └── http_test.go ├── LICENSE ├── README.md ├── doc.go ├── g11n.go └── g11n_test.go /test/doc.go: -------------------------------------------------------------------------------- 1 | // Package test contains functions used in g11n testing. 2 | package test 3 | -------------------------------------------------------------------------------- /locale/json_loader.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | type jsonLoader struct{} 9 | 10 | func (jl *jsonLoader) Load(fileName string) map[string]string { 11 | result := map[string]string{} 12 | if data, err := ioutil.ReadFile(fileName); err == nil { 13 | json.Unmarshal(data, &result) 14 | } 15 | return result 16 | } 17 | 18 | func init() { 19 | RegisterLoader("json", &jsonLoader{}) 20 | } 21 | -------------------------------------------------------------------------------- /locale/yaml_loader.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "gopkg.in/yaml.v2" 7 | ) 8 | 9 | type yamlLoader struct{} 10 | 11 | func (yl *yamlLoader) Load(fileName string) map[string]string { 12 | result := map[string]string{} 13 | if data, err := ioutil.ReadFile(fileName); err == nil { 14 | yaml.Unmarshal(data, &result) 15 | } 16 | return result 17 | } 18 | 19 | func init() { 20 | RegisterLoader("yaml", &yamlLoader{}) 21 | } 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.15.2 4 | 5 | before_install: 6 | - go get -v golang.org/x/tools/cmd/cover 7 | - go get -v github.com/axw/gocov/gocov 8 | - go get -u golang.org/x/lint/golint 9 | 10 | install: 11 | - go install -race -v std 12 | - go get -race -t -v ./... 13 | - go install -race -v ./... 14 | 15 | script: 16 | - "$HOME/gopath/bin/golint ." 17 | - go test -timeout 1s -cpu=2 -race -v ./... 18 | - go test -timeout 1s -cpu=2 -covermode=atomic ./... 19 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/sgatev/g11n" 7 | 8 | "golang.org/x/text/language" 9 | ) 10 | 11 | // SetLocale sets the locale of a MessageFactory from HTTP Request value. 12 | func SetLocale(mf *g11n.MessageFactory, r *http.Request) { 13 | acceptLanguage := r.Header.Get("Accept-Language") 14 | preferred, _, _ := language.ParseAcceptLanguage(acceptLanguage) 15 | 16 | var matcher = language.NewMatcher(mf.Locales()) 17 | tag, _, _ := matcher.Match(preferred...) 18 | 19 | mf.LoadLocale(tag) 20 | } 21 | -------------------------------------------------------------------------------- /locale/loader.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | // Loader represents a locale loader for a specific file format. 4 | type Loader interface { 5 | 6 | // Load loads the locale from the file exposing a map of translated messages. 7 | Load(fileName string) map[string]string 8 | } 9 | 10 | var loaders = map[string]Loader{} 11 | 12 | // GetLoader returns the locale loader for a specific format. 13 | func GetLoader(format string) (Loader, bool) { 14 | loader, ok := loaders[format] 15 | return loader, ok 16 | } 17 | 18 | // RegisterLoader registers a locale loader for specific format. 19 | func RegisterLoader(format string, loader Loader) { 20 | loaders[format] = loader 21 | } 22 | -------------------------------------------------------------------------------- /locale/doc.go: -------------------------------------------------------------------------------- 1 | // Package locale presents localization loaders for the g11n internationalization library. 2 | // 3 | // 4 | // I. Creating a locale loader 5 | // 6 | // Every locale loader (built-in or custom) should implement the Loader interface and register itself in the loaders registry under some specific format name using RegisterLoader. 7 | // 8 | // func init() { 9 | // RegisterLoader("custom", customLoader{}) 10 | // } 11 | // 12 | // 13 | // II. Retrieving a locale loader 14 | // 15 | // A locale loader could be retrieved from the loaders registry using GetLoader. 16 | // 17 | // loader, ok := GetLoader("custom") (Loader, bool) 18 | // 19 | // 20 | // III. Built-in locale loaders 21 | // 22 | // g11n comes with two built-in locale loaders - "json" and "yaml". 23 | package locale 24 | -------------------------------------------------------------------------------- /test/utils.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | ) 7 | 8 | // MustPanic expects a panic in the current goroutine and verifies its 9 | // message against an expected one. 10 | func MustPanic(t *testing.T, expectedMessage string) { 11 | if r := recover(); r == nil { 12 | t.Errorf("The code did not panic.") 13 | } else { 14 | if actualMessage := r.(string); expectedMessage != actualMessage { 15 | t.Errorf("The panic message was not correct.\n"+ 16 | "\tExpected: %v\n"+ 17 | "\tActual: %v\n", expectedMessage, actualMessage) 18 | } 19 | } 20 | } 21 | 22 | // TempFile creates a temporary file with some content and returns the file name. 23 | func TempFile(content string) string { 24 | file, err := ioutil.TempFile("", "g11n") 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | ioutil.WriteFile(file.Name(), []byte(content), 0644) 30 | 31 | return file.Name() 32 | } 33 | -------------------------------------------------------------------------------- /locale/yaml_loader_test.go: -------------------------------------------------------------------------------- 1 | package locale_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | . "github.com/sgatev/g11n/locale" 8 | . "github.com/sgatev/g11n/test" 9 | ) 10 | 11 | func testLoadYaml(t *testing.T, filePath string, expected map[string]string) { 12 | loader, _ := GetLoader("yaml") 13 | 14 | if actual := loader.Load(filePath); !reflect.DeepEqual(actual, expected) { 15 | t.Errorf("") 16 | } 17 | } 18 | 19 | func TestLoadCorrectYaml(t *testing.T) { 20 | filePath := TempFile(` 21 | M.MyLittleSomething: Котка 22 | `) 23 | 24 | testLoadYaml(t, filePath, map[string]string{ 25 | "M.MyLittleSomething": "Котка", 26 | }) 27 | } 28 | 29 | func TestLoadIncorrectYaml(t *testing.T) { 30 | filePath := TempFile(` 31 | M.MyLittleSomething - Котка 32 | `) 33 | 34 | testLoadYaml(t, filePath, map[string]string{}) 35 | } 36 | 37 | func TestLoadYamlWithDuplicateKeys(t *testing.T) { 38 | filePath := TempFile(` 39 | M.MyLittleSomething: First 40 | M.MyLittleSomething: Second 41 | `) 42 | 43 | testLoadYaml(t, filePath, map[string]string{ 44 | "M.MyLittleSomething": "Second", 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Stanislav Gatev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /locale/json_loader_test.go: -------------------------------------------------------------------------------- 1 | package locale_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | . "github.com/sgatev/g11n/locale" 8 | . "github.com/sgatev/g11n/test" 9 | ) 10 | 11 | func testLoadJson(t *testing.T, filePath string, expected map[string]string) { 12 | loader, _ := GetLoader("json") 13 | 14 | if actual := loader.Load(filePath); !reflect.DeepEqual(actual, expected) { 15 | t.Errorf("") 16 | } 17 | } 18 | 19 | func TestLoadCorrectJson(t *testing.T) { 20 | filePath := TempFile(` 21 | { 22 | "M.MyLittleSomething": "Котка" 23 | } 24 | `) 25 | 26 | testLoadJson(t, filePath, map[string]string{ 27 | "M.MyLittleSomething": "Котка", 28 | }) 29 | } 30 | 31 | func TestLoadIncorrectJson(t *testing.T) { 32 | filePath := TempFile(` 33 | 34 | "M.MyLittleSomething": "Котка" 35 | } 36 | `) 37 | 38 | testLoadJson(t, filePath, map[string]string{}) 39 | } 40 | 41 | func TestLoadJsonWithDuplicateKeys(t *testing.T) { 42 | filePath := TempFile(` 43 | { 44 | "M.MyLittleSomething": "First", 45 | "M.MyLittleSomething": "Second" 46 | } 47 | `) 48 | 49 | testLoadJson(t, filePath, map[string]string{ 50 | "M.MyLittleSomething": "Second", 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /http/http_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "golang.org/x/text/language" 8 | 9 | . "github.com/sgatev/g11n" 10 | . "github.com/sgatev/g11n/http" 11 | . "github.com/sgatev/g11n/test" 12 | ) 13 | 14 | func testMessage(t *testing.T, actual, expected string) { 15 | if actual != expected { 16 | t.Errorf("Message is not the same as expected.\n"+ 17 | "\tActual: %v\n"+ 18 | "\tExpected: %v\n", actual, expected) 19 | } 20 | } 21 | 22 | func TestSetLocaleFromRequest(t *testing.T) { 23 | type M struct { 24 | MyLittleSomething func() string `default:"cat"` 25 | } 26 | 27 | bgLocale := TempFile(` 28 | { 29 | "M.MyLittleSomething": "котка" 30 | } 31 | `) 32 | 33 | esLocale := TempFile(` 34 | { 35 | "M.MyLittleSomething": "gato" 36 | } 37 | `) 38 | 39 | factory := New() 40 | 41 | factory.SetLocales(map[language.Tag]string{ 42 | language.Bulgarian: bgLocale, 43 | language.Spanish: esLocale, 44 | }, "json") 45 | 46 | r, _ := http.NewRequest("GET", "https://golang.org", nil) 47 | r.Header.Add("Accept-Language", "bg") 48 | 49 | SetLocale(factory, r) 50 | 51 | m := factory.Init(&M{}).(*M) 52 | 53 | testMessage(t, 54 | string(m.MyLittleSomething()), 55 | `котка`) 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # g11n 2 | 3 | [![Join the chat at https://gitter.im/sgatev/g11n](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/sgatev/g11n?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | [![Build Status](https://travis-ci.org/sgatev/g11n.svg?branch=master)](https://travis-ci.org/sgatev/g11n) 5 | [![Coverage Status](https://coveralls.io/repos/sgatev/g11n/badge.svg?branch=master&service=github)](https://coveralls.io/github/sgatev/g11n?branch=master) 6 | [![Go Report Card](http://goreportcard.com/badge/sgatev/g11n)](http://goreportcard.com/report/sgatev/g11n) 7 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/sgatev/g11n) 8 | [![MIT License](http://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) 9 | 10 | **g11n** */gopherization/* is an internationalization library inspired by [GWT](http://www.gwtproject.org/doc/latest/DevGuideI18nMessages.html) that offers: 11 | 12 | * **Statically-typed** message keys. 13 | * **Parameterized** messages. 14 | * **Extendable** message formatting. 15 | * **Custom** localization **file format**. 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "net/http" 23 | 24 | "github.com/sgatev/g11n" 25 | locale "github.com/sgatev/g11n/http" 26 | ) 27 | 28 | type Messages struct { 29 | Hello func(string) string `default:"Hi %v!"` 30 | } 31 | 32 | func main() { 33 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 34 | // Create messages factory. 35 | factory := g11n.New() 36 | 37 | // Initialize messages value. 38 | var m Messages 39 | factory.Init(&m) 40 | 41 | // Set messages locale. 42 | locale.SetLocale(factory, r) 43 | 44 | fmt.Fprintf(w, m.Hello("World")) 45 | }) 46 | 47 | log.Fatal(http.ListenAndServe(":8080", nil)) 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package g11n is an internationalization library that offers: 2 | // 3 | // * Statically-typed message keys. 4 | // * Parameterized messages. 5 | // * Extendable message formatting. 6 | // * Custom localization file format. 7 | // 8 | // 9 | // I. Initialization 10 | // 11 | // Create a new instance of g11n. Each instance handles messages and locales separately. 12 | // 13 | // G = g11n.New() 14 | // 15 | // Define a struct with messages. 16 | // 17 | // type Messages struct { 18 | // TheAnswer func(string, int) string `default:"The answer to %v is %v."` 19 | // } 20 | // 21 | // Initialize an instance of the struct through the g11n object. 22 | // 23 | // var M *Messages 24 | // G.Init(M) 25 | // 26 | // Invoke messages on that instance. 27 | // 28 | // M.TheAnswer("everything", 42") 29 | // 30 | // 31 | // II. Choosing locale 32 | // 33 | // Load a locale in the g11n instance. 34 | // 35 | // G.LoadLocale("json", "bg", bgLocalePath) 36 | // 37 | // Different locale loaders could be registered by implementing the locale.Loader 38 | // interface. 39 | // 40 | // Specify the locale for every message struct initialized by this g11n instance. 41 | // 42 | // G.SetLocale("en") 43 | // 44 | // 45 | // III. Format parameters 46 | // 47 | // The parameters of a message call could be formatted by declaring a special 48 | // type that implements 49 | // 50 | // G11nParam() string 51 | // 52 | // The format method G11nParam is invoked before substituting a parameter in the message. 53 | // 54 | // type PluralFormat int 55 | // 56 | // func (pf PluralFormat) G11nParam() string { 57 | // switch pf { 58 | // case 0: 59 | // return "some" 60 | // case 1: 61 | // return "crazy" 62 | // default: 63 | // return "stuff" 64 | // } 65 | // } 66 | // 67 | // type M struct { 68 | // MyLittleSomething func(PluralFormat) string `default:"Count: %v"` 69 | // } 70 | // 71 | // 72 | // IV. Format result 73 | // 74 | // The result of a message call could be further formatted by declaring a special 75 | // result type that implements 76 | // 77 | // G11nResult(formattedMessage string) string 78 | // 79 | // The format method G11nResult is invoked after all parameters have been substituted in the message. 80 | // 81 | // type SafeHTMLFormat string 82 | // 83 | // func (shf SafeHTMLFormat) G11nResult(formattedMessage string) string { 84 | // r := strings.NewReplacer("<", `\<`, ">", `\>`, "/", `\/`) 85 | // return r.Replace(formattedMessage) 86 | // } 87 | // 88 | // type M struct { 89 | // MyLittleSomething func() SafeHTMLFormat `default:"Oops!"` 90 | // } 91 | package g11n 92 | -------------------------------------------------------------------------------- /g11n.go: -------------------------------------------------------------------------------- 1 | package g11n 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | g11nLocale "github.com/sgatev/g11n/locale" 8 | 9 | "golang.org/x/text/language" 10 | ) 11 | 12 | // Application constants. 13 | const ( 14 | defaultMessageTag = "default" 15 | ) 16 | 17 | // Error message patterns. 18 | const ( 19 | wrongResultsCountMessage = "Wrong number of results in a g11n message. Expected 1, got %v." 20 | unknownFormatMessage = "Unknown locale format '%v'." 21 | unknownLocaleTag = "Unknown locale '%v'." 22 | ) 23 | 24 | // paramFormatter represents a type that supports custom formatting 25 | // when it is used as parameter in a call to a g11n message. 26 | type paramFormatter interface { 27 | 28 | // G11nParam formats a type in a specific way when passed to a g11n message. 29 | G11nParam() string 30 | } 31 | 32 | // resultFormatter represents a type that supports custom formatting 33 | // when it is returned from a call to a g11n message. 34 | type resultFormatter interface { 35 | 36 | // G11nResult accepts a formatted g11n message and modifies it before returning. 37 | G11nResult(formattedMessage string) string 38 | } 39 | 40 | type stringInitializer func() 41 | 42 | // formatParam extracts the data from a reflected argument value and returns it. 43 | func formatParam(value reflect.Value) interface{} { 44 | valueInterface := value.Interface() 45 | 46 | if paramFormatter, ok := valueInterface.(paramFormatter); ok { 47 | return paramFormatter.G11nParam() 48 | } 49 | 50 | return valueInterface 51 | } 52 | 53 | // localeInfo encapsulates the data required to parse a localization file. 54 | type localeInfo struct { 55 | format string 56 | path string 57 | } 58 | 59 | // MessageFactory initializes message structs and provides language 60 | // translations to messages. 61 | type MessageFactory struct { 62 | locales map[language.Tag]localeInfo 63 | dictionary map[string]string 64 | stringInitializers []stringInitializer 65 | } 66 | 67 | // New returns a fresh G11n message factory. 68 | func New() *MessageFactory { 69 | return &MessageFactory{ 70 | dictionary: map[string]string{}, 71 | locales: map[language.Tag]localeInfo{}, 72 | } 73 | } 74 | 75 | // Locales returns the registered locales in a message factory. 76 | func (mf *MessageFactory) Locales() []language.Tag { 77 | locales := make([]language.Tag, 0, len(mf.locales)) 78 | 79 | for locale := range mf.locales { 80 | locales = append(locales, locale) 81 | } 82 | 83 | return locales 84 | } 85 | 86 | // SetLocale registers a locale file in the specified format. 87 | func (mf *MessageFactory) SetLocale(tag language.Tag, format, path string) { 88 | mf.locales[tag] = localeInfo{ 89 | format: format, 90 | path: path, 91 | } 92 | } 93 | 94 | // SetLocales registers locale files in the specified format. 95 | func (mf *MessageFactory) SetLocales(locales map[language.Tag]string, format string) { 96 | for tag, path := range locales { 97 | mf.SetLocale(tag, format, path) 98 | } 99 | } 100 | 101 | // LoadLocale sets the currently active locale for the messages generated 102 | // by this factory. 103 | func (mf *MessageFactory) LoadLocale(tag language.Tag) { 104 | locale, ok := mf.locales[tag] 105 | if !ok { 106 | panic(fmt.Sprintf(unknownLocaleTag, tag)) 107 | } 108 | 109 | loader, ok := g11nLocale.GetLoader(locale.format) 110 | if !ok { 111 | panic(fmt.Sprintf(unknownFormatMessage, locale.format)) 112 | } 113 | 114 | mf.dictionary = loader.Load(locale.path) 115 | 116 | for _, initializer := range mf.stringInitializers { 117 | initializer() 118 | } 119 | } 120 | 121 | // Init initializes the message fields of a structure pointer. 122 | func (mf *MessageFactory) Init(structPtr interface{}) interface{} { 123 | mf.initializeStruct(structPtr) 124 | 125 | return structPtr 126 | } 127 | 128 | // messageHandler creates a handler that formats a message based on provided parameters. 129 | func (mf *MessageFactory) messageHandler(messagePattern, messageKey string, resultType reflect.Type) func([]reflect.Value) []reflect.Value { 130 | return func(args []reflect.Value) []reflect.Value { 131 | // Extract localized message. 132 | if message, ok := mf.dictionary[messageKey]; ok { 133 | messagePattern = message 134 | } 135 | 136 | // Format message parameters. 137 | var formattedParams []interface{} 138 | for _, arg := range args { 139 | formattedParams = append(formattedParams, formatParam(arg)) 140 | } 141 | 142 | // Find the result message value. 143 | message := fmt.Sprintf(messagePattern, formattedParams...) 144 | messageValue := reflect.ValueOf(message) 145 | 146 | // Format message result. 147 | resultValue := reflect.New(resultType).Elem() 148 | if resultFormatter, ok := resultValue.Interface().(resultFormatter); ok { 149 | formattedResult := resultFormatter.G11nResult(message) 150 | messageValue = reflect.ValueOf(formattedResult).Convert(resultType) 151 | } 152 | resultValue.Set(messageValue) 153 | 154 | return []reflect.Value{resultValue} 155 | } 156 | } 157 | 158 | // initializeStruct initializes the message fields of a struct pointer. 159 | func (mf *MessageFactory) initializeStruct(structPtr interface{}) { 160 | instance := reflect.Indirect(reflect.ValueOf(structPtr)) 161 | concreteType := instance.Type() 162 | 163 | // Initialize each message func of the struct. 164 | for i := 0; i < concreteType.NumField(); i++ { 165 | field := concreteType.Field(i) 166 | instanceField := instance.FieldByName(field.Name) 167 | 168 | if field.Anonymous { 169 | mf.initializeEmbeddedStruct(field, instanceField) 170 | } else { 171 | mf.initializeField(concreteType, field, instanceField) 172 | } 173 | } 174 | } 175 | 176 | // initializeEmbeddedStruct initializes the message fields of an embedded struct. 177 | func (mf *MessageFactory) initializeEmbeddedStruct( 178 | field reflect.StructField, 179 | instanceField reflect.Value) { 180 | 181 | // Create the embedded struct. 182 | embeddedStruct := reflect.New(field.Type.Elem()) 183 | instanceField.Set(embeddedStruct) 184 | 185 | // Initialize the messages of the embedded struct. 186 | mf.initializeStruct(embeddedStruct.Interface()) 187 | } 188 | 189 | // initializeField initializes a message field. 190 | func (mf *MessageFactory) initializeField( 191 | concreteType reflect.Type, 192 | field reflect.StructField, 193 | instanceField reflect.Value) { 194 | 195 | messageKey := fmt.Sprintf("%v.%v", concreteType.Name(), field.Name) 196 | 197 | // Extract default message. 198 | messagePattern := field.Tag.Get(defaultMessageTag) 199 | 200 | if field.Type.Kind() == reflect.String { 201 | // Initialize string field. 202 | 203 | message := messagePattern 204 | 205 | // Format message result. 206 | if resultFormatter, ok := instanceField.Interface().(resultFormatter); ok { 207 | message = resultFormatter.G11nResult(message) 208 | } 209 | 210 | mf.stringInitializers = append(mf.stringInitializers, func() { 211 | message := messagePattern 212 | 213 | // Extract localized message. 214 | if messagePattern, ok := mf.dictionary[messageKey]; ok { 215 | message = messagePattern 216 | } 217 | 218 | instanceField.SetString(message) 219 | }) 220 | 221 | instanceField.SetString(message) 222 | } else { 223 | // Initialize func field. 224 | 225 | // Check if return type of the message func is correct. 226 | if field.Type.NumOut() != 1 { 227 | panic(fmt.Sprintf(wrongResultsCountMessage, field.Type.NumOut())) 228 | } 229 | 230 | resultType := field.Type.Out(0) 231 | 232 | // Create proxy function for handling the message. 233 | messageProxyFunc := reflect.MakeFunc( 234 | field.Type, mf.messageHandler(messagePattern, messageKey, resultType)) 235 | 236 | instanceField.Set(messageProxyFunc) 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /g11n_test.go: -------------------------------------------------------------------------------- 1 | package g11n_test 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "golang.org/x/text/language" 9 | 10 | . "github.com/sgatev/g11n" 11 | . "github.com/sgatev/g11n/test" 12 | ) 13 | 14 | func testMessage(t *testing.T, actual, expected string) { 15 | if actual != expected { 16 | t.Errorf("Message is not the same as expected.\n"+ 17 | "\tActual: %v\n"+ 18 | "\tExpected: %v\n", actual, expected) 19 | } 20 | } 21 | 22 | func TestInitVar(t *testing.T) { 23 | type M struct { 24 | MyLittleSomething func() string `default:"Not as quick as the brown fox."` 25 | } 26 | 27 | var m M 28 | New().Init(&m) 29 | 30 | testMessage(t, 31 | m.MyLittleSomething(), 32 | "Not as quick as the brown fox.") 33 | } 34 | 35 | func TestStringReturnType(t *testing.T) { 36 | type M struct { 37 | MyLittleSomething string `default:"Not as quick as the brown fox."` 38 | } 39 | 40 | m := New().Init(&M{}).(*M) 41 | 42 | testMessage(t, 43 | m.MyLittleSomething, 44 | "Not as quick as the brown fox.") 45 | } 46 | 47 | func TestCustomReturnType(t *testing.T) { 48 | type M struct { 49 | MyLittleSomething SafeHTMLFormat `default:"Oops!"` 50 | } 51 | 52 | m := New().Init(&M{}).(*M) 53 | 54 | testMessage(t, 55 | string(m.MyLittleSomething), 56 | `\Oops!\<\/message\>`) 57 | } 58 | 59 | func TestSimpleMessage(t *testing.T) { 60 | type M struct { 61 | MyLittleSomething func() string `default:"Not as quick as the brown fox."` 62 | } 63 | 64 | m := New().Init(&M{}).(*M) 65 | 66 | testMessage(t, 67 | m.MyLittleSomething(), 68 | "Not as quick as the brown fox.") 69 | } 70 | 71 | func TestInitEmbeddedStruct(t *testing.T) { 72 | type N struct { 73 | MyLittleSomething func() string `default:"Not as quick as the brown fox."` 74 | } 75 | 76 | type M struct { 77 | *N 78 | } 79 | 80 | m := New().Init(&M{}).(*M) 81 | 82 | testMessage(t, 83 | m.MyLittleSomething(), 84 | "Not as quick as the brown fox.") 85 | } 86 | 87 | func TestMessageWithNumberArguments(t *testing.T) { 88 | type M struct { 89 | MyLittleSomething func(int, float64) string `default:"And yeah, it works: %v %v"` 90 | } 91 | 92 | m := New().Init(&M{}).(*M) 93 | 94 | testMessage(t, 95 | m.MyLittleSomething(42, 3.14), 96 | "And yeah, it works: 42 3.14") 97 | } 98 | 99 | func TestMessageWithCustomFormat(t *testing.T) { 100 | type M struct { 101 | MyLittleSomething func(CustomFormat) string `default:"Surprise! %v"` 102 | } 103 | 104 | m := New().Init(&M{}).(*M) 105 | 106 | testMessage(t, 107 | m.MyLittleSomething(CustomFormat{func() string { 108 | return "This works" 109 | }}), 110 | "Surprise! This works") 111 | } 112 | 113 | func TestPluralMessage(t *testing.T) { 114 | type M struct { 115 | MyLittleSomething func(PluralFormat) string `default:"Count: %v"` 116 | } 117 | 118 | m := New().Init(&M{}).(*M) 119 | 120 | testMessage(t, 121 | m.MyLittleSomething(0), 122 | "Count: some") 123 | testMessage(t, 124 | m.MyLittleSomething(1), 125 | "Count: crazy") 126 | testMessage(t, 127 | m.MyLittleSomething(21), 128 | "Count: stuff") 129 | } 130 | 131 | func TestMessageWithDifferentResult(t *testing.T) { 132 | type M struct { 133 | MyLittleSomething func() SafeHTMLFormat `default:"Oops!"` 134 | } 135 | 136 | m := New().Init(&M{}).(*M) 137 | 138 | testMessage(t, 139 | string(m.MyLittleSomething()), 140 | `\Oops!\<\/message\>`) 141 | } 142 | 143 | func TestMessageWithMultipleResults(t *testing.T) { 144 | type M struct { 145 | MyLittleSomething func() (string, int) `default:"Oops!"` 146 | } 147 | 148 | defer MustPanic(t, "Wrong number of results in a g11n message. Expected 1, got 2.") 149 | 150 | New().Init(&M{}) 151 | } 152 | 153 | func TestLocalizedMessageSingle(t *testing.T) { 154 | type M struct { 155 | MyLittleSomething func() SafeHTMLFormat `default:"Cat"` 156 | } 157 | 158 | bgLocale := TempFile(` 159 | { 160 | "M.MyLittleSomething": "Котка" 161 | } 162 | `) 163 | 164 | factory := New() 165 | 166 | factory.SetLocale(language.Bulgarian, "json", bgLocale) 167 | factory.LoadLocale(language.Bulgarian) 168 | 169 | m := factory.Init(&M{}).(*M) 170 | 171 | testMessage(t, 172 | string(m.MyLittleSomething()), 173 | `Котка`) 174 | } 175 | 176 | func TestLocalizedMessageMultiple(t *testing.T) { 177 | type M struct { 178 | MyLittleSomething func() SafeHTMLFormat `default:"Cat"` 179 | } 180 | 181 | bgLocale := TempFile(` 182 | { 183 | "M.MyLittleSomething": "Котка" 184 | } 185 | `) 186 | 187 | factory := New() 188 | 189 | factory.SetLocales(map[language.Tag]string{ 190 | language.Bulgarian: bgLocale, 191 | }, "json") 192 | factory.LoadLocale(language.Bulgarian) 193 | 194 | m := factory.Init(&M{}).(*M) 195 | 196 | testMessage(t, 197 | string(m.MyLittleSomething()), 198 | `Котка`) 199 | } 200 | 201 | func TestSetLocaleAfterInitFunc(t *testing.T) { 202 | type M struct { 203 | MyLittleSomething func() string `default:"Cat"` 204 | } 205 | 206 | bgLocale := TempFile(` 207 | { 208 | "M.MyLittleSomething": "Котка" 209 | } 210 | `) 211 | 212 | factory := New() 213 | 214 | m := factory.Init(&M{}).(*M) 215 | 216 | factory.SetLocale(language.Bulgarian, "json", bgLocale) 217 | factory.LoadLocale(language.Bulgarian) 218 | 219 | testMessage(t, 220 | string(m.MyLittleSomething()), 221 | `Котка`) 222 | } 223 | 224 | func TestSetLocaleAfterInitString(t *testing.T) { 225 | type M struct { 226 | MyLittleSomething string `default:"Cat"` 227 | } 228 | 229 | bgLocale := TempFile(` 230 | { 231 | "M.MyLittleSomething": "Котка" 232 | } 233 | `) 234 | 235 | factory := New() 236 | 237 | m := factory.Init(&M{}).(*M) 238 | 239 | factory.SetLocale(language.Bulgarian, "json", bgLocale) 240 | factory.LoadLocale(language.Bulgarian) 241 | 242 | testMessage(t, 243 | string(m.MyLittleSomething), 244 | `Котка`) 245 | } 246 | 247 | func TestLocalizedMessageUnknownFormat(t *testing.T) { 248 | type M struct { 249 | MyLittleSomething func() SafeHTMLFormat `default:"Cat"` 250 | } 251 | 252 | bgLocale := TempFile(` 253 | M.MyLittleSomething: Котка 254 | `) 255 | 256 | defer MustPanic(t, "Unknown locale format 'custom'.") 257 | 258 | factory := New() 259 | factory.SetLocale(language.Bulgarian, "custom", bgLocale) 260 | factory.LoadLocale(language.Bulgarian) 261 | } 262 | 263 | func TestLocales(t *testing.T) { 264 | factory := New() 265 | 266 | factory.SetLocales(map[language.Tag]string{ 267 | language.Bulgarian: "", 268 | language.Spanish: "", 269 | language.Italian: "", 270 | }, "custom") 271 | 272 | expected := map[language.Tag]struct{}{ 273 | language.Bulgarian: struct{}{}, 274 | language.Spanish: struct{}{}, 275 | language.Italian: struct{}{}, 276 | } 277 | actual := map[language.Tag]struct{}{} 278 | for _, tag := range factory.Locales() { 279 | actual[tag] = struct{}{} 280 | } 281 | 282 | if !reflect.DeepEqual(actual, expected) { 283 | t.Errorf("Locales are not correct.\n"+ 284 | "Expected: %v\n"+ 285 | "Actual: %v\n", expected, actual) 286 | } 287 | } 288 | 289 | func TestUnknownLocale(t *testing.T) { 290 | defer MustPanic(t, "Unknown locale 'bg'.") 291 | 292 | factory := New() 293 | factory.LoadLocale(language.Bulgarian) 294 | } 295 | 296 | type CustomFormat struct { 297 | message func() string 298 | } 299 | 300 | func (cf CustomFormat) G11nParam() string { 301 | return cf.message() 302 | } 303 | 304 | type PluralFormat int 305 | 306 | func (pf PluralFormat) G11nParam() string { 307 | switch pf { 308 | case 0: 309 | return "some" 310 | case 1: 311 | return "crazy" 312 | default: 313 | return "stuff" 314 | } 315 | } 316 | 317 | type SafeHTMLFormat string 318 | 319 | func (shf SafeHTMLFormat) G11nResult(formattedMessage string) string { 320 | r := strings.NewReplacer("<", `\<`, ">", `\>`, "/", `\/`) 321 | return r.Replace(formattedMessage) 322 | } 323 | --------------------------------------------------------------------------------