You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

i18n.go 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. // Copyright 2014 The Macaron Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License"): you may
  4. // not use this file except in compliance with the License. You may obtain
  5. // a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. // License for the specific language governing permissions and limitations
  13. // under the License.
  14. // Package i18n is a middleware that provides app Internationalization and Localization of Macaron.
  15. package i18n
  16. import (
  17. "fmt"
  18. "path"
  19. "strings"
  20. "github.com/Unknwon/com"
  21. "github.com/Unknwon/i18n"
  22. "golang.org/x/text/language"
  23. "gopkg.in/macaron.v1"
  24. )
  25. const _VERSION = "0.4.0"
  26. func Version() string {
  27. return _VERSION
  28. }
  29. // initLocales initializes language type list and Accept-Language header matcher.
  30. func initLocales(opt Options) language.Matcher {
  31. tags := make([]language.Tag, len(opt.Langs))
  32. for i, lang := range opt.Langs {
  33. tags[i] = language.Raw.Make(lang)
  34. fname := fmt.Sprintf(opt.Format, lang)
  35. // Append custom locale file.
  36. custom := []interface{}{}
  37. customPath := path.Join(opt.CustomDirectory, fname)
  38. if com.IsFile(customPath) {
  39. custom = append(custom, customPath)
  40. }
  41. var locale interface{}
  42. if data, ok := opt.Files[fname]; ok {
  43. locale = data
  44. } else {
  45. locale = path.Join(opt.Directory, fname)
  46. }
  47. err := i18n.SetMessageWithDesc(lang, opt.Names[i], locale, custom...)
  48. if err != nil && err != i18n.ErrLangAlreadyExist {
  49. panic(fmt.Errorf("fail to set message file(%s): %v", lang, err))
  50. }
  51. }
  52. return language.NewMatcher(tags)
  53. }
  54. // A Locale describles the information of localization.
  55. type Locale struct {
  56. i18n.Locale
  57. }
  58. // Language returns language current locale represents.
  59. func (l Locale) Language() string {
  60. return l.Lang
  61. }
  62. // Options represents a struct for specifying configuration options for the i18n middleware.
  63. type Options struct {
  64. // Suburl of path. Default is empty.
  65. SubURL string
  66. // Directory to load locale files. Default is "conf/locale"
  67. Directory string
  68. // File stores actual data of locale files. Used for in-memory purpose.
  69. Files map[string][]byte
  70. // Custom directory to overload locale files. Default is "custom/conf/locale"
  71. CustomDirectory string
  72. // Langauges that will be supported, order is meaningful.
  73. Langs []string
  74. // Human friendly names corresponding to Langs list.
  75. Names []string
  76. // Default language locale, leave empty to remain unset.
  77. DefaultLang string
  78. // Locale file naming style. Default is "locale_%s.ini".
  79. Format string
  80. // Name of language parameter name in URL. Default is "lang".
  81. Parameter string
  82. // Redirect when user uses get parameter to specify language.
  83. Redirect bool
  84. // Name that maps into template variable. Default is "i18n".
  85. TmplName string
  86. // Configuration section name. Default is "i18n".
  87. Section string
  88. // Domain used for `lang` cookie. Default is ""
  89. CookieDomain string
  90. }
  91. func prepareOptions(options []Options) Options {
  92. var opt Options
  93. if len(options) > 0 {
  94. opt = options[0]
  95. }
  96. if len(opt.Section) == 0 {
  97. opt.Section = "i18n"
  98. }
  99. sec := macaron.Config().Section(opt.Section)
  100. opt.SubURL = strings.TrimSuffix(opt.SubURL, "/")
  101. if len(opt.Langs) == 0 {
  102. opt.Langs = sec.Key("LANGS").Strings(",")
  103. }
  104. if len(opt.Names) == 0 {
  105. opt.Names = sec.Key("NAMES").Strings(",")
  106. }
  107. if len(opt.Langs) == 0 {
  108. panic("no language is specified")
  109. } else if len(opt.Langs) != len(opt.Names) {
  110. panic("length of langs is not same as length of names")
  111. }
  112. i18n.SetDefaultLang(opt.DefaultLang)
  113. if len(opt.Directory) == 0 {
  114. opt.Directory = sec.Key("DIRECTORY").MustString("conf/locale")
  115. }
  116. if len(opt.CustomDirectory) == 0 {
  117. opt.CustomDirectory = sec.Key("CUSTOM_DIRECTORY").MustString("custom/conf/locale")
  118. }
  119. if len(opt.Format) == 0 {
  120. opt.Format = sec.Key("FORMAT").MustString("locale_%s.ini")
  121. }
  122. if len(opt.Parameter) == 0 {
  123. opt.Parameter = sec.Key("PARAMETER").MustString("lang")
  124. }
  125. if !opt.Redirect {
  126. opt.Redirect = sec.Key("REDIRECT").MustBool()
  127. }
  128. if len(opt.TmplName) == 0 {
  129. opt.TmplName = sec.Key("TMPL_NAME").MustString("i18n")
  130. }
  131. return opt
  132. }
  133. type LangType struct {
  134. Lang, Name string
  135. }
  136. // I18n is a middleware provides localization layer for your application.
  137. // Paramenter langs must be in the form of "en-US", "zh-CN", etc.
  138. // Otherwise it may not recognize browser input.
  139. func I18n(options ...Options) macaron.Handler {
  140. opt := prepareOptions(options)
  141. m := initLocales(opt)
  142. return func(ctx *macaron.Context) {
  143. isNeedRedir := false
  144. hasCookie := false
  145. // 1. Check URL arguments.
  146. lang := ctx.Query(opt.Parameter)
  147. // 2. Get language information from cookies.
  148. if len(lang) == 0 {
  149. lang = ctx.GetCookie("lang")
  150. hasCookie = true
  151. } else {
  152. isNeedRedir = true
  153. }
  154. // Check again in case someone modify by purpose.
  155. if !i18n.IsExist(lang) {
  156. lang = ""
  157. isNeedRedir = false
  158. hasCookie = false
  159. }
  160. // 3. Get language information from 'Accept-Language'.
  161. // The first element in the list is chosen to be the default language automatically.
  162. if len(lang) == 0 {
  163. tags, _, _ := language.ParseAcceptLanguage(ctx.Req.Header.Get("Accept-Language"))
  164. tag, _, _ := m.Match(tags...)
  165. lang = tag.String()
  166. isNeedRedir = false
  167. }
  168. curLang := LangType{
  169. Lang: lang,
  170. }
  171. // Save language information in cookies.
  172. if !hasCookie {
  173. ctx.SetCookie("lang", curLang.Lang, 1<<31-1, "/"+strings.TrimPrefix(opt.SubURL, "/"), opt.CookieDomain)
  174. }
  175. restLangs := make([]LangType, 0, i18n.Count()-1)
  176. langs := i18n.ListLangs()
  177. names := i18n.ListLangDescs()
  178. for i, v := range langs {
  179. if lang != v {
  180. restLangs = append(restLangs, LangType{v, names[i]})
  181. } else {
  182. curLang.Name = names[i]
  183. }
  184. }
  185. // Set language properties.
  186. locale := Locale{i18n.Locale{lang}}
  187. ctx.Map(locale)
  188. ctx.Locale = locale
  189. ctx.Data[opt.TmplName] = locale
  190. ctx.Data["Tr"] = i18n.Tr
  191. ctx.Data["Lang"] = locale.Lang
  192. ctx.Data["LangName"] = curLang.Name
  193. ctx.Data["AllLangs"] = append([]LangType{curLang}, restLangs...)
  194. ctx.Data["RestLangs"] = restLangs
  195. if opt.Redirect && isNeedRedir {
  196. ctx.Redirect(opt.SubURL + ctx.Req.RequestURI[:strings.Index(ctx.Req.RequestURI, "?")])
  197. }
  198. }
  199. }