diff --git a/_examples/miscellaneous/i18n/hosts b/_examples/miscellaneous/i18n/hosts new file mode 100644 index 00000000..fb394e4b --- /dev/null +++ b/_examples/miscellaneous/i18n/hosts @@ -0,0 +1,26 @@ +# Copyright (c) 1993-2009 Microsoft Corp. +# +# This is a sample HOSTS file used by Microsoft TCP/IP for Windows. +# +# This file contains the mappings of IP addresses to host names. Each +# entry should be kept on an individual line. The IP address should +# be placed in the first column followed by the corresponding host name. +# The IP address and the host name should be separated by at least one +# space. +# +# Additionally, comments (such as these) may be inserted on individual +# lines or following the machine name denoted by a '#' symbol. +# +# For example: +# +# 102.54.94.97 rhino.acme.com # source server +# 38.25.63.10 x.acme.com # x client host + +# localhost name resolution is handled within DNS itself. +# 127.0.0.1 localhost +# ::1 localhost +127.0.0.1 mydomain.com +127.0.0.1 en.mydomain.com +127.0.0.1 el.mydomain.com +127.0.0.1 el-gr.mydomain.com +127.0.0.1 zh.mydomain.com \ No newline at end of file diff --git a/_examples/miscellaneous/i18n/main.go b/_examples/miscellaneous/i18n/main.go index 1eb9e310..b0aaabde 100644 --- a/_examples/miscellaneous/i18n/main.go +++ b/_examples/miscellaneous/i18n/main.go @@ -6,31 +6,34 @@ import ( ) var i18nConfig = i18n.Config{ - Default: "en-US", - URLParameter: "lang", // optional. - PathParameter: "lang", // optional. + Default: "en-US", 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. }, + // Optionals. 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. }, + URLParameter: "lang", + Subdomain: true, + // Cookie: "lang", + // SetCookie: false, + // Indentifier: func(ctx iris.Context) string { return "zh-CN" }, } func newApp() *iris.Application { app := iris.New() + i18nMiddleware := i18n.NewI18n(i18nConfig) + app.Use(i18nMiddleware.Handler()) + // See https://github.com/kataras/iris/issues/1369 // if you want to enable this (SEO) feature (OPTIONAL). - i18nWrapper := i18n.NewWrapper(i18nConfig) - app.WrapRouter(i18nWrapper) - - i18nMiddleware := i18n.New(i18nConfig) - app.Use(i18nMiddleware) + app.WrapRouter(i18nMiddleware.Wrapper()) app.Get("/", func(ctx iris.Context) { // Ir tries to find the language by: @@ -105,9 +108,10 @@ func newApp() *iris.Application { func main() { app := newApp() - // go to http://localhost:8080/el-GR/some-path - // or http://localhost:8080/zh-cn/templates - // or http://localhost:8080/some-path?lang=el-GR + // go to http://localhost:8080/el-gr/some-path (by path prefix) + // or http://el.mydomain.com8080/some-path (by subdomain - test locally with the hosts file) + // or http://localhost:8080/zh-CN/templates (by path prefix with uppercase) + // or http://localhost:8080/some-path?lang=el-GR (by url parameter) // or http://localhost:8080 (default is en-US) // or http://localhost:8080/?lang=zh-CN // diff --git a/middleware/i18n/i18n.go b/middleware/i18n/i18n.go index 3440e653..318ac5a9 100644 --- a/middleware/i18n/i18n.go +++ b/middleware/i18n/i18n.go @@ -28,22 +28,22 @@ type Config struct { // // Checked: Serving state, runtime. URLParameter string - // PathParameter is the name of the path parameter which the language can be indentified, - // e.g. "lang" for "{lang:string}". + // Cookie is the key of the request cookie which the language can be indentified, + // e.g. "lang". // // Checked: Serving state, runtime. - // - // You can set custom handler to set the language too. - // Example: - // setLangMiddleware := func(ctx iris.Context){ - // langKey := ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey() - // languageByPath := ctx.Params().Get("lang") // see {lang} - // ctx.Values().Set(langKey, languageByPath) - // ctx.Next() - // } - // app.Use(setLangMiddleware) - // app.Use(theI18nMiddlewareInstance) - PathParameter string + Cookie string + // If SetCookie is true and Cookie field is not empty + // then it will set the cookie to the language found by y Context's Value's "lang" key or URLParameter or Cookie or Indentifier. + // Defaults to false. + SetCookie bool + + // If Subdomain is true then it will try to map a subdomain + // with a valid language from the language list or alternatives. + Subdomain bool + + // Indentifier is a function which the language can be indentified if the above URLParameter and Cookie failed to. + Indentifier func(context.Context) string // Languages is a map[string]string which the key is the language i81n and the value is the file location. // @@ -56,10 +56,6 @@ type Config struct { // Languages: map[string]string{"en-US": "./locales/en-US.ini"} set // Alternatives: map[string]string{ "en":"en-US", "english": "en-US"}. Alternatives map[string]string - - // If SetCookie is true then it will set the cookie to the language found by URLParameter, PathParameter or by Context's Value's "lang" key. - // Defaults to false. - SetCookie bool } // Exists returns true if the language, or something similar @@ -139,79 +135,139 @@ func (c *Config) loadLanguages() { }) } -// test file: ../../_examples/miscellaneous/i18n/main_test.go -type i18nMiddleware struct { +// I18n is the structure which keeps the i18n configuration and implement all Iris i18n features. +type I18n struct { config Config } -// New returns a new i18n middleware. -func New(c Config) context.Handler { - c.loadLanguages() - i := &i18nMiddleware{config: c} - return i.ServeHTTP +// NewI18n returns a new i18n middleware which contains +// the middleware itself and a router wrapper. +func NewI18n(config Config) *I18n { + config.loadLanguages() + return &I18n{config} } -// ServeHTTP serves the request, the actual middleware's job is located here. -func (i *i18nMiddleware) ServeHTTP(ctx context.Context) { - wasByCookie := false +// Handler returns the middleware handler. +func (i *I18n) Handler() context.Handler { + return func(ctx context.Context) { + wasByCookie := false - langKey := ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey() - language := ctx.Values().GetString(langKey) - if language == "" { - // try to get by path parameter - if i.config.PathParameter != "" { - language = ctx.Params().Get(i.config.PathParameter) - } + langKey := ctx.Application().ConfigurationReadOnly().GetTranslateLanguageContextKey() + language := ctx.Values().GetString(langKey) 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) + if i.config.URLParameter != "" { + // try to get by url parameter + language = ctx.URLParam(i.config.URLParameter) + } - if len(language) > 0 { - wasByCookie = true - } else { + if language == "" { + if i.config.Cookie != "" { + // then try to take the lang field from the cookie + language = ctx.GetCookie(i.config.Cookie) + wasByCookie = language != "" + } + + if language == "" && i.config.Subdomain { + if subdomain := ctx.Subdomain(); subdomain != "" { + if lang, ok := i.config.Exists(subdomain); ok { + language = lang + } + } + } + + if language == "" { // try to get by the request headers. langHeader := ctx.GetHeader("Accept-Language") if len(langHeader) > 0 { for _, langEntry := range strings.Split(langHeader, ",") { lc := strings.Split(langEntry, ";")[0] - if lc, ok := i.config.Exists(lc); ok { - language = lc + if lang, ok := i.config.Exists(lc); ok { + language = lang break } } } } + + if language == "" && i.config.Indentifier != nil { + language = i.config.Indentifier(ctx) + } } } + + 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 + } + + // if it was not taken by the cookie, then set the cookie in order to have it. + if !wasByCookie && i.config.SetCookie && i.config.Cookie != "" { + ctx.SetCookieKV(i.config.Cookie, language) + } + + ctx.Values().Set(langKey, language) + + // Set iris.translate and iris.translateLang functions (they can be passed to templates as they are later on). + ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetTranslateFunctionContextKey(), getTranslateFunction(language)) + // Note: translate (global) language function input argument should match exactly, case-sensitive and "Alternatives" field is not part of the fetch progress. + ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetTranslateLangFunctionContextKey(), i18n.Tr) + + ctx.Next() } +} - if language == "" { - language = i.config.Default +// Wrapper returns a new router wrapper. +// The result function can be passed on `Application.WrapRouter`. +// It compares the path prefix for translated language and +// local redirects the requested path with the selected (from the path) language to the router. +// +// In order this to work as expected, it should be combined with `Application.Use(i.Handler())` +// which registers the i18n middleware itself. +func (i *I18n) Wrapper() func(http.ResponseWriter, *http.Request, http.HandlerFunc) { + return func(w http.ResponseWriter, r *http.Request, routerHandler http.HandlerFunc) { + found := false + path := r.URL.Path[1:] + + if idx := strings.IndexByte(path, '/'); idx > 0 { + path = path[:idx] + } + + if path != "" { + if lang, ok := i.config.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) + found = true + } + } + + if !found && i.config.Subdomain { + host := context.GetHost(r) + if dotIdx := strings.IndexByte(host, '.'); dotIdx > 0 { + subdomain := host[0:dotIdx] + if subdomain != "" { + if lang, ok := i.config.Exists(subdomain); ok { + host = host[dotIdx+1:] + r.URL.Host = host + r.Host = host + r.Header.Set("Accept-Language", lang) + } + } + } + } + + routerHandler(w, r) } - - // 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 - } - - // if it was not taken by the cookie, then set the cookie in order to have it. - if !wasByCookie && i.config.SetCookie { - ctx.SetCookieKV(langKey, language) - } - - ctx.Values().Set(langKey, language) - - // Set iris.translate and iris.translateLang functions (they can be passed to templates as they are later on). - ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetTranslateFunctionContextKey(), getTranslateFunction(language)) - // Note: translate (global) language function input argument should match exactly, case-sensitive and "Alternatives" field is not part of the fetch progress. - ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetTranslateLangFunctionContextKey(), i18n.Tr) - - ctx.Next() } func getTranslateFunction(lang string) func(string, ...interface{}) string { @@ -220,6 +276,11 @@ func getTranslateFunction(lang string) func(string, ...interface{}) string { } } +// New returns a new i18n middleware. +func New(config Config) context.Handler { + return NewI18n(config).Handler() +} + // NewWrapper accepts a Config and returns a new router wrapper. // The result function can be passed on `Application.WrapRouter`. // It compares the path prefix for translated language and @@ -227,30 +288,8 @@ func getTranslateFunction(lang string) func(string, ...interface{}) string { // // In order this to work as expected, it should be combined with `Application.Use(New)` // which registers the i18n middleware itself. -func NewWrapper(c Config) func(http.ResponseWriter, *http.Request, http.HandlerFunc) { - c.loadLanguages() - - return func(w http.ResponseWriter, r *http.Request, routerHandler http.HandlerFunc) { - path := r.URL.Path[1:] - - if idx := strings.IndexRune(path, '/'); idx > 0 { - path = path[:idx] - } - - 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) - } - } - - routerHandler(w, r) - } +func NewWrapper(config Config) func(http.ResponseWriter, *http.Request, http.HandlerFunc) { + return NewI18n(config).Wrapper() } // Translate returns the translated word from a context based on the current selected locale.