From eb3328dbe9b2d1593212f91b2f632ff28abdbbe3 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 20 Nov 2019 02:35:41 +0200 Subject: [PATCH] add tests for #1369 and various improvements Former-commit-id: 2fe1f077cf5b6a0fb32a27cf86462fea776a7d58 --- _examples/miscellaneous/i18n/main.go | 47 +++++++------- _examples/miscellaneous/i18n/main_test.go | 52 +++++++++++---- iris.go | 2 +- middleware/i18n/i18n.go | 77 +++++++++++++++-------- 4 files changed, 116 insertions(+), 62 deletions(-) diff --git a/_examples/miscellaneous/i18n/main.go b/_examples/miscellaneous/i18n/main.go index 6afdd300..1eb9e310 100644 --- a/_examples/miscellaneous/i18n/main.go +++ b/_examples/miscellaneous/i18n/main.go @@ -5,25 +5,29 @@ import ( "github.com/kataras/iris/v12/middleware/i18n" ) +var i18nConfig = i18n.Config{ + Default: "en-US", + URLParameter: "lang", // optional. + PathParameter: "lang", // optional. + Languages: map[string]string{ + "en-US": "./locales/locale_en-US.ini", // maps to en-US, en-us and en. + "el-GR": "./locales/locale_el-GR.ini", // maps to el-GR, el-gr and el. + "zh-CN": "./locales/locale_zh-CN.ini", // maps to zh-CN, zh-cn and zh. + }, + Alternatives: map[string]string{ // optional. + "english": "en-US", // now english maps to en-US + "greek": "el-GR", // and greek to el-GR + "chinese": "zh-CN", // and chinese to zh-CN too. + }, +} + func newApp() *iris.Application { app := iris.New() - app.Logger().SetLevel("debug") - - i18nConfig := i18n.Config{ - Default: "en-US", - URLParameter: "lang", - PathParameter: "lang", - Languages: map[string]string{ - "en-US": "./locales/locale_en-US.ini", - "el-GR": "./locales/locale_el-GR.ini", - "zh-CN": "./locales/locale_zh-CN.ini", - }, - Alternatives: map[string]string{"greek": "el-GR"}, - } // See https://github.com/kataras/iris/issues/1369 - // if you want to enable this (SEO) feature. - app.WrapRouter(i18n.NewWrapper(i18nConfig)) + // if you want to enable this (SEO) feature (OPTIONAL). + i18nWrapper := i18n.NewWrapper(i18nConfig) + app.WrapRouter(i18nWrapper) i18nMiddleware := i18n.New(i18nConfig) app.Use(i18nMiddleware) @@ -37,12 +41,10 @@ func newApp() *iris.Application { // it tries to find the language by the "language" cookie // if didn't found then it it set to the Default set on the configuration - // hi is the key, 'iris' is the %s on the .ini file + // hi is the key/word, 'iris' is the %s on the .ini file // the second parameter is optional - // hi := ctx.Translate("hi", "iris") - // or: - hi := i18n.Translate(ctx, "hi", "iris") + hi := ctx.Translate("hi", "iris") // GetTranslateLanguageContextKey() == "language" language := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey()) @@ -61,6 +63,9 @@ func newApp() *iris.Application { ctx.WriteString("sitemap") }) + // Note: It is highly recommended to use one and no more i18n middleware instances at a time, + // the first one was already passed by `app.Use` above. + // This middleware which registers on "/multi" route is here just for the shake of the example. multiLocale := i18n.New(i18n.Config{ Default: "en-US", URLParameter: "lang", @@ -73,8 +78,8 @@ func newApp() *iris.Application { app.Get("/multi", multiLocale, func(ctx iris.Context) { language := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey()) - fromFirstFileValue := i18n.Translate(ctx, "key1") - fromSecondFileValue := i18n.Translate(ctx, "key2") + fromFirstFileValue := ctx.Translate("key1") + fromSecondFileValue := ctx.Translate("key2") ctx.Writef("From the language: %s, translated output:\n%s=%s\n%s=%s", language, "key1", fromFirstFileValue, "key2", fromSecondFileValue) diff --git a/_examples/miscellaneous/i18n/main_test.go b/_examples/miscellaneous/i18n/main_test.go index 5c9b9b4b..809b8a5c 100644 --- a/_examples/miscellaneous/i18n/main_test.go +++ b/_examples/miscellaneous/i18n/main_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "strings" "testing" "github.com/kataras/iris/v12/httptest" @@ -12,9 +13,11 @@ func TestI18n(t *testing.T) { expectedf := "From the language %s translated output: %s" var ( - elgr = fmt.Sprintf(expectedf, "el-GR", "γεια, iris") - enus = fmt.Sprintf(expectedf, "en-US", "hello, iris") - zhcn = fmt.Sprintf(expectedf, "zh-CN", "您好,iris") + tests = map[string]string{ + "en-US": fmt.Sprintf(expectedf, "en-US", "hello, iris"), + "el-GR": fmt.Sprintf(expectedf, "el-GR", "γεια, iris"), + "zh-CN": fmt.Sprintf(expectedf, "zh-CN", "您好,iris"), + } elgrMulti = fmt.Sprintf("From the language: %s, translated output:\n%s=%s\n%s=%s", "el-GR", "key1", @@ -29,20 +32,43 @@ func TestI18n(t *testing.T) { ) e := httptest.New(t, app) - // default is en-US - e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal(enus) - // default is en-US if lang query unable to be found - e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal(enus) + // default should be en-US. + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal(tests["en-US"]) - e.GET("/").WithQueryString("lang=el-GR").Expect().Status(httptest.StatusOK). - Body().Equal(elgr) - e.GET("/").WithQueryString("lang=en-US").Expect().Status(httptest.StatusOK). - Body().Equal(enus) - e.GET("/").WithQueryString("lang=zh-CN").Expect().Status(httptest.StatusOK). - Body().Equal(zhcn) + for lang, body := range tests { + e.GET("/").WithQueryString("lang=" + lang).Expect().Status(httptest.StatusOK). + Body().Equal(body) + + // test lowercase. + e.GET("/").WithQueryString("lang=" + strings.ToLower(lang)).Expect().Status(httptest.StatusOK). + Body().Equal(body) + + // test first part (e.g. en instead of en-US). + langFirstPart := strings.Split(lang, "-")[0] + e.GET("/").WithQueryString("lang=" + langFirstPart).Expect().Status(httptest.StatusOK). + Body().Equal(body) + + // test accept-language header prefix (i18n wrapper). + e.GET("/"+lang).WithHeader("Accept-Language", lang).Expect().Status(httptest.StatusOK). + Body().Equal(body) + + // test path prefix (i18n router wrapper). + e.GET("/" + lang).Expect().Status(httptest.StatusOK). + Body().Equal(body) + + // test path prefix with first part. + e.GET("/" + langFirstPart).Expect().Status(httptest.StatusOK). + Body().Equal(body) + } e.GET("/multi").WithQueryString("lang=el-GR").Expect().Status(httptest.StatusOK). Body().Equal(elgrMulti) e.GET("/multi").WithQueryString("lang=en-US").Expect().Status(httptest.StatusOK). Body().Equal(enusMulti) + + // test path prefix (i18n router wrapper). + e.GET("/el-gr/multi").Expect().Status(httptest.StatusOK). + Body().Equal(elgrMulti) + e.GET("/en/multi").Expect().Status(httptest.StatusOK). + Body().Equal(enusMulti) } diff --git a/iris.go b/iris.go index ae545bba..768ec2a7 100644 --- a/iris.go +++ b/iris.go @@ -806,7 +806,7 @@ func Raw(f func() error) Runner { // It builds the default router with its default macros // and the template functions that are very-closed to iris. // -// If error occured while building the Application, the returns type of error will be an *errgroup.Group +// If error occurred while building the Application, the returns type of error will be an *errgroup.Group // which let the callers to inspect the errors and cause, usage: // // import "github.com/kataras/iris/v12/core/errgroup" diff --git a/middleware/i18n/i18n.go b/middleware/i18n/i18n.go index 13f4bc97..3440e653 100644 --- a/middleware/i18n/i18n.go +++ b/middleware/i18n/i18n.go @@ -7,11 +7,16 @@ import ( "net/http" "reflect" "strings" + "sync" "github.com/iris-contrib/i18n" "github.com/kataras/iris/v12/context" ) +// If `Config.Default` is missing and `Config.Languages` or `Config.Alternatives` contains this key then it will set as the default locale, +// no need to be exported(see `Config.Default`). +const defLang = "en-US" + // Config the i18n options. type Config struct { // Default set it if you want a default language. @@ -71,6 +76,16 @@ func (c *Config) Exists(lang string) (string, bool) { return i18n.IsExistSimilar(lang) } +// all locale files passed, we keep them in order +// to check if a file is already passed by `New` or `NewWrapper`, +// because we don't have a way to check before the appending of +// a locale file and the same locale code can be used more than one to register different file names (at runtime too). +var ( + localeFilesSet = make(map[string]struct{}) + localesMutex sync.RWMutex + once sync.Once +) + func (c *Config) loadLanguages() { if len(c.Languages) == 0 { panic("field Languages is empty") @@ -82,14 +97,8 @@ func (c *Config) loadLanguages() { } } - firstlanguage := "" // load the files for k, langFileOrFiles := range c.Languages { - if i18n.IsExist(k) { - // if it is already stored through middleware (`New`) then skip it. - continue - } - // remove all spaces. langFileOrFiles = strings.Replace(langFileOrFiles, " ", "", -1) // note: if only one, then the first element is the "v". @@ -100,21 +109,34 @@ func (c *Config) loadLanguages() { v += ".ini" } - err := i18n.SetMessage(k, v) - if err != nil && err != i18n.ErrLangAlreadyExist { - panic(fmt.Sprintf("Failed to set locale file' %s' with error: %v", k, err)) - } - if firstlanguage == "" { - firstlanguage = k + localesMutex.RLock() + _, exists := localeFilesSet[v] + localesMutex.RUnlock() + if !exists { + localesMutex.Lock() + err := i18n.SetMessage(k, v) + // fmt.Printf("add %s = %s\n", k, v) + if err != nil && err != i18n.ErrLangAlreadyExist { + panic(fmt.Sprintf("Failed to set locale file' %s' with error: %v", k, err)) + } + + localeFilesSet[v] = struct{}{} + localesMutex.Unlock() } + } } - // if not default language set then set to the first of the "Languages". + if c.Default == "" { - c.Default = firstlanguage + if lang, ok := c.Exists(defLang); ok { + c.Default = lang + } } - i18n.SetDefaultLang(c.Default) + once.Do(func() { // set global default lang once. + // fmt.Printf("set default language: %s\n", c.Default) + i18n.SetDefaultLang(c.Default) + }) } // test file: ../../_examples/miscellaneous/i18n/main_test.go @@ -144,7 +166,6 @@ func (i *i18nMiddleware) ServeHTTP(ctx context.Context) { if language == "" { // try to get by url parameter language = ctx.URLParam(i.config.URLParameter) - if language == "" { // then try to take the lang field from the cookie language = ctx.GetCookie(langKey) @@ -168,14 +189,14 @@ func (i *i18nMiddleware) ServeHTTP(ctx context.Context) { } } + if language == "" { + language = i.config.Default + } + // returns the original key of the language and true // when the language, or something similar exists (e.g. en-US maps to en). if lc, ok := i.config.Exists(language); ok { language = lc - } else { - // if unexpected language given, the middleware will translate to the default language, - // the language key should be also this language instead of the user-given. - language = i.config.Default } // if it was not taken by the cookie, then set the cookie in order to have it. @@ -216,14 +237,16 @@ func NewWrapper(c Config) func(http.ResponseWriter, *http.Request, http.HandlerF path = path[:idx] } - if lang, ok := c.Exists(path); ok { - path = r.URL.Path[len(path)+1:] - if path == "" { - path = "/" + if path != "" { + if lang, ok := c.Exists(path); ok { + path = r.URL.Path[len(path)+1:] + if path == "" { + path = "/" + } + r.RequestURI = path + r.URL.Path = path + r.Header.Set("Accept-Language", lang) } - r.RequestURI = path - r.URL.Path = path - r.Header.Set("Accept-Language", lang) } routerHandler(w, r)