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.

242 lines
6.7 KiB

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