├── 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 | [](https://gitter.im/sgatev/g11n?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
4 | [](https://travis-ci.org/sgatev/g11n)
5 | [](https://coveralls.io/github/sgatev/g11n?branch=master)
6 | [](http://goreportcard.com/report/sgatev/g11n)
7 | [](https://godoc.org/github.com/sgatev/g11n)
8 | [](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 |
--------------------------------------------------------------------------------