// Copyright 2014 The Macaron Authors // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. // Package i18n provides an Internationalization and Localization middleware for Macaron applications. package i18n import ( "fmt" "log" "os" "path" "strings" "gitea.com/macaron/macaron" "github.com/unknwon/i18n" "golang.org/x/text/language" ) const _VERSION = "0.4.0" func Version() string { return _VERSION } // isFile returns true if given path is a file, // or returns false when it's a directory or does not exist. func isFile(filePath string) bool { f, e := os.Stat(filePath) if e != nil { return false } return !f.IsDir() } // initLocales initializes language type list and Accept-Language header matcher. func initLocales(opt Options) language.Matcher { tags := make([]language.Tag, len(opt.Langs)) for i, lang := range opt.Langs { tags[i] = language.Raw.Make(lang) fname := fmt.Sprintf(opt.Format, lang) // Append custom locale file. custom := []interface{}{} customPath := path.Join(opt.CustomDirectory, fname) if isFile(customPath) { custom = append(custom, customPath) } var locale interface{} if data, ok := opt.Files[fname]; ok { locale = data } else { locale = path.Join(opt.Directory, fname) } err := i18n.SetMessageWithDesc(lang, opt.Names[i], locale, custom...) if err != nil && err != i18n.ErrLangAlreadyExist { log.Printf("ERROR: failed to set message file(%s) for language %s: %v", lang, opt.Names[i], err) } } return language.NewMatcher(tags) } // A Locale describles the information of localization. type Locale struct { i18n.Locale } // Language returns language current locale represents. func (l Locale) Language() string { return l.Lang } // Options represents a struct for specifying configuration options for the i18n middleware. type Options struct { // Suburl of path. Default is empty. SubURL string // Directory to load locale files. Default is "conf/locale" Directory string // File stores actual data of locale files. Used for in-memory purpose. Files map[string][]byte // Custom directory to overload locale files. Default is "custom/conf/locale" CustomDirectory string // Langauges that will be supported, order is meaningful. Langs []string // Human friendly names corresponding to Langs list. Names []string // Default language locale, leave empty to remain unset. DefaultLang string // Locale file naming style. Default is "locale_%s.ini". Format string // Name of language parameter name in URL. Default is "lang". Parameter string // Redirect when user uses get parameter to specify language. Redirect bool // Name that maps into template variable. Default is "i18n". TmplName string // Configuration section name. Default is "i18n". Section string // Domain used for `lang` cookie. Default is "" CookieDomain string // Set the Secure flag on the `lang` cookie. Default is disabled. Secure bool // Set the HTTP Only flag on the `lang` cookie. Default is disabled. CookieHttpOnly bool } func prepareOptions(options []Options) Options { var opt Options if len(options) > 0 { opt = options[0] } if len(opt.Section) == 0 { opt.Section = "i18n" } sec := macaron.Config().Section(opt.Section) opt.SubURL = strings.TrimSuffix(opt.SubURL, "/") if len(opt.Langs) == 0 { opt.Langs = sec.Key("LANGS").Strings(",") } if len(opt.Names) == 0 { opt.Names = sec.Key("NAMES").Strings(",") } if len(opt.Langs) == 0 { panic("no language is specified") } else if len(opt.Langs) != len(opt.Names) { panic("length of langs is not same as length of names") } i18n.SetDefaultLang(opt.DefaultLang) if len(opt.Directory) == 0 { opt.Directory = sec.Key("DIRECTORY").MustString("conf/locale") } if len(opt.CustomDirectory) == 0 { opt.CustomDirectory = sec.Key("CUSTOM_DIRECTORY").MustString("custom/conf/locale") } if len(opt.Format) == 0 { opt.Format = sec.Key("FORMAT").MustString("locale_%s.ini") } if len(opt.Parameter) == 0 { opt.Parameter = sec.Key("PARAMETER").MustString("lang") } if !opt.Redirect { opt.Redirect = sec.Key("REDIRECT").MustBool() } if len(opt.TmplName) == 0 { opt.TmplName = sec.Key("TMPL_NAME").MustString("i18n") } return opt } type LangType struct { Lang, Name string } // I18n is a middleware provides localization layer for your application. // Paramenter langs must be in the form of "en-US", "zh-CN", etc. // Otherwise it may not recognize browser input. func I18n(options ...Options) macaron.Handler { opt := prepareOptions(options) m := initLocales(opt) return func(ctx *macaron.Context) { isNeedRedir := false hasCookie := false // 1. Check URL arguments. lang := ctx.Query(opt.Parameter) // 2. Get language information from cookies. if len(lang) == 0 { lang = ctx.GetCookie("lang") hasCookie = true } else { isNeedRedir = true } // Check again in case someone modify by purpose. if !i18n.IsExist(lang) { lang = "" isNeedRedir = false hasCookie = false } // 3. Get language information from 'Accept-Language'. // The first element in the list is chosen to be the default language automatically. if len(lang) == 0 { tags, _, _ := language.ParseAcceptLanguage(ctx.Req.Header.Get("Accept-Language")) tag, _, _ := m.Match(tags...) lang = tag.String() isNeedRedir = false } curLang := LangType{ Lang: lang, } // Save language information in cookies. if !hasCookie { ctx.SetCookie("lang", curLang.Lang, 1<<31-1, "/"+strings.TrimPrefix(opt.SubURL, "/"), opt.CookieDomain, opt.Secure, opt.CookieHttpOnly) } restLangs := make([]LangType, 0, i18n.Count()-1) langs := i18n.ListLangs() names := i18n.ListLangDescs() for i, v := range langs { if lang != v { restLangs = append(restLangs, LangType{v, names[i]}) } else { curLang.Name = names[i] } } // Set language properties. locale := Locale{Locale: i18n.Locale{Lang: lang}} ctx.Map(locale) ctx.Locale = locale ctx.Data[opt.TmplName] = locale ctx.Data["Tr"] = i18n.Tr ctx.Data["Lang"] = locale.Lang ctx.Data["LangName"] = curLang.Name ctx.Data["AllLangs"] = append([]LangType{curLang}, restLangs...) ctx.Data["RestLangs"] = restLangs if opt.Redirect && isNeedRedir { ctx.Redirect(opt.SubURL + path.Clean(ctx.Req.RequestURI[:strings.Index(ctx.Req.RequestURI, "?")])) } } }