i18n.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. // Package i18n provides internalization and localization features for Iris.
  2. // To use with net/http see https://github.com/kataras/i18n instead.
  3. package i18n
  4. import (
  5. "fmt"
  6. "net/http"
  7. "os"
  8. "strings"
  9. "sync"
  10. "github.com/kataras/iris/context"
  11. "github.com/kataras/iris/core/router"
  12. "golang.org/x/text/language"
  13. )
  14. type (
  15. // Loader accepts a `Matcher` and should return a `Localizer`.
  16. // Functions that implement this type should load locale files.
  17. Loader func(m *Matcher) (Localizer, error)
  18. // Localizer is the interface which returned from a `Loader`.
  19. // Types that implement this interface should be able to retrieve a `Locale`
  20. // based on the language index.
  21. Localizer interface {
  22. // GetLocale should return a valid `Locale` based on the language index.
  23. // It will always match the Loader.Matcher.Languages[index].
  24. // It may return the default language if nothing else matches based on custom localizer's criteria.
  25. GetLocale(index int) context.Locale
  26. }
  27. )
  28. // I18n is the structure which keeps the i18n configuration and implements localization and internationalization features.
  29. type I18n struct {
  30. localizer Localizer
  31. matcher *Matcher
  32. loader Loader
  33. mu sync.Mutex
  34. // ExtractFunc is the type signature for declaring custom logic
  35. // to extract the language tag name.
  36. // Defaults to nil.
  37. ExtractFunc func(ctx *context.Context) string
  38. // If not empty, it is language identifier by url query.
  39. //
  40. // Defaults to "lang".
  41. URLParameter string
  42. // If not empty, it is language identifier by cookie of this name.
  43. //
  44. // Defaults to empty.
  45. Cookie string
  46. // If true then a subdomain can be a language identifier.
  47. //
  48. // Defaults to true.
  49. Subdomain bool
  50. // If true then it will return empty string when translation for a a specific language's key was not found.
  51. // Defaults to false, fallback defaultLang:key will be used.
  52. Strict bool
  53. // If true then Iris will wrap its router with the i18n router wrapper on its Build state.
  54. // It will (local) redirect requests like:
  55. // 1. /$lang_prefix/$path to /$path with the language set to $lang_prefix part.
  56. // 2. $lang_subdomain.$domain/$path to $domain/$path with the language set to $lang_subdomain part.
  57. //
  58. // Defaults to true.
  59. PathRedirect bool
  60. }
  61. var _ context.I18nReadOnly = (*I18n)(nil)
  62. // makeTags converts language codes to language Tags.
  63. func makeTags(languages ...string) (tags []language.Tag) {
  64. for _, lang := range languages {
  65. tag, err := language.Parse(lang)
  66. if err == nil && tag != language.Und {
  67. tags = append(tags, tag)
  68. }
  69. }
  70. return
  71. }
  72. // New returns a new `I18n` instance. Use its `Load` or `LoadAssets` to load languages.
  73. func New() *I18n {
  74. return &I18n{
  75. URLParameter: "lang",
  76. Subdomain: true,
  77. PathRedirect: true,
  78. }
  79. }
  80. // Load is a method shortcut to load files using a filepath.Glob pattern.
  81. // It returns a non-nil error on failure.
  82. //
  83. // See `New` and `Glob` package-level functions for more.
  84. func (i *I18n) Load(globPattern string, languages ...string) error {
  85. return i.Reset(Glob(globPattern), languages...)
  86. }
  87. // LoadAssets is a method shortcut to load files using go-bindata.
  88. // It returns a non-nil error on failure.
  89. //
  90. // See `New` and `Asset` package-level functions for more.
  91. func (i *I18n) LoadAssets(assetNames func() []string, asset func(string) ([]byte, error), languages ...string) error {
  92. return i.Reset(Assets(assetNames, asset), languages...)
  93. }
  94. // Reset sets the locales loader and languages.
  95. // It is not meant to be used by users unless
  96. // a custom `Loader` must be used instead of the default one.
  97. func (i *I18n) Reset(loader Loader, languages ...string) error {
  98. tags := makeTags(languages...)
  99. i.loader = loader
  100. i.matcher = &Matcher{
  101. strict: len(tags) > 0,
  102. Languages: tags,
  103. matcher: language.NewMatcher(tags),
  104. }
  105. return i.reload()
  106. }
  107. // reload loads the language files from the provided Loader,
  108. // the `New` package-level function preloads those files already.
  109. func (i *I18n) reload() error { // May be an exported function, if requested.
  110. i.mu.Lock()
  111. defer i.mu.Unlock()
  112. if i.loader == nil {
  113. return fmt.Errorf("nil loader")
  114. }
  115. localizer, err := i.loader(i.matcher)
  116. if err != nil {
  117. return err
  118. }
  119. i.localizer = localizer
  120. return nil
  121. }
  122. // Loaded reports whether `New` or `Load/LoadAssets` called.
  123. func (i *I18n) Loaded() bool {
  124. return i != nil && i.loader != nil && i.localizer != nil && i.matcher != nil
  125. }
  126. // Tags returns the registered languages or dynamically resolved by files.
  127. // Use `Load` or `LoadAssets` first.
  128. func (i *I18n) Tags() []language.Tag {
  129. if !i.Loaded() {
  130. return nil
  131. }
  132. return i.matcher.Languages
  133. }
  134. // SetDefault changes the default language.
  135. // Please avoid using this method; the default behavior will accept
  136. // the first language of the registered tags as the default one.
  137. func (i *I18n) SetDefault(langCode string) bool {
  138. t, err := language.Parse(langCode)
  139. if err != nil {
  140. return false
  141. }
  142. if tag, index, conf := i.matcher.Match(t); conf > language.Low {
  143. if l, ok := i.localizer.(interface {
  144. SetDefault(int) bool
  145. }); ok {
  146. if l.SetDefault(index) {
  147. tags := i.matcher.Languages
  148. // set the order
  149. tags[index] = tags[0]
  150. tags[0] = tag
  151. i.matcher.Languages = tags
  152. i.matcher.matcher = language.NewMatcher(tags)
  153. return true
  154. }
  155. }
  156. }
  157. return false
  158. }
  159. // Matcher implements the languae.Matcher.
  160. // It contains the original language Matcher and keeps an ordered
  161. // list of the registered languages for further use (see `Loader` implementation).
  162. type Matcher struct {
  163. strict bool
  164. Languages []language.Tag
  165. matcher language.Matcher
  166. }
  167. var _ language.Matcher = (*Matcher)(nil)
  168. // Match returns the best match for any of the given tags, along with
  169. // a unique index associated with the returned tag and a confidence
  170. // score.
  171. func (m *Matcher) Match(t ...language.Tag) (language.Tag, int, language.Confidence) {
  172. return m.matcher.Match(t...)
  173. }
  174. // MatchOrAdd acts like Match but it checks and adds a language tag, if not found,
  175. // when the `Matcher.strict` field is true (when no tags are provided by the caller)
  176. // and they should be dynamically added to the list.
  177. func (m *Matcher) MatchOrAdd(t language.Tag) (tag language.Tag, index int, conf language.Confidence) {
  178. tag, index, conf = m.Match(t)
  179. if conf <= language.Low && !m.strict {
  180. // not found, add it now.
  181. m.Languages = append(m.Languages, t)
  182. tag = t
  183. index = len(m.Languages) - 1
  184. conf = language.Exact
  185. m.matcher = language.NewMatcher(m.Languages) // reset matcher to include the new language.
  186. }
  187. return
  188. }
  189. // ParseLanguageFiles returns a map of language indexes and
  190. // their associated files based on the "fileNames".
  191. func (m *Matcher) ParseLanguageFiles(fileNames []string) (map[int][]string, error) {
  192. languageFiles := make(map[int][]string)
  193. for _, fileName := range fileNames {
  194. index := parsePath(m, fileName)
  195. if index == -1 {
  196. continue
  197. }
  198. languageFiles[index] = append(languageFiles[index], fileName)
  199. }
  200. return languageFiles, nil
  201. }
  202. func parsePath(m *Matcher, path string) int {
  203. if t, ok := parseLanguage(path); ok {
  204. if _, index, conf := m.MatchOrAdd(t); conf > language.Low {
  205. return index
  206. }
  207. }
  208. return -1
  209. }
  210. func reverseStrings(s []string) []string {
  211. for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
  212. s[i], s[j] = s[j], s[i]
  213. }
  214. return s
  215. }
  216. func parseLanguage(path string) (language.Tag, bool) {
  217. if idx := strings.LastIndexByte(path, '.'); idx > 0 {
  218. path = path[0:idx]
  219. }
  220. // path = strings.ReplaceAll(path, "..", "")
  221. names := strings.FieldsFunc(path, func(r rune) bool {
  222. return r == '_' || r == os.PathSeparator || r == '/' || r == '.'
  223. })
  224. names = reverseStrings(names) // see https://github.com/kataras/i18n/issues/1
  225. for _, s := range names {
  226. t, err := language.Parse(s)
  227. if err != nil {
  228. continue
  229. }
  230. return t, true
  231. }
  232. return language.Und, false
  233. }
  234. // TryMatchString will try to match the "s" with a registered language tag.
  235. // It returns -1 as the language index and false if not found.
  236. func (i *I18n) TryMatchString(s string) (language.Tag, int, bool) {
  237. if tag, err := language.Parse(s); err == nil {
  238. if tag, index, conf := i.matcher.Match(tag); conf > language.Low {
  239. return tag, index, true
  240. }
  241. }
  242. return language.Und, -1, false
  243. }
  244. // Tr returns a translated message based on the "lang" language code
  245. // and its key(format) with any optional arguments attached to it.
  246. //
  247. // It returns an empty string if "format" not matched.
  248. func (i *I18n) Tr(lang, format string, args ...interface{}) string {
  249. _, index, ok := i.TryMatchString(lang)
  250. if !ok {
  251. index = 0
  252. }
  253. loc := i.localizer.GetLocale(index)
  254. if loc != nil {
  255. msg := loc.GetMessage(format, args...)
  256. if msg == "" && !i.Strict && index > 0 {
  257. // it's not the default/fallback language and not message found for that lang:key.
  258. return i.localizer.GetLocale(0).GetMessage(format, args...)
  259. }
  260. return msg
  261. }
  262. return ""
  263. }
  264. const acceptLanguageHeaderKey = "Accept-Language"
  265. // GetLocale returns the found locale of a request.
  266. // It will return the first registered language if nothing else matched.
  267. func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
  268. var (
  269. index int
  270. ok bool
  271. )
  272. if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" {
  273. if v := ctx.Values().GetString(contextKey); v != "" {
  274. if v == "default" {
  275. index = 0 // no need to call `TryMatchString` and spend time.
  276. } else {
  277. _, index, _ = i.TryMatchString(v)
  278. }
  279. locale := i.localizer.GetLocale(index)
  280. if locale == nil {
  281. return nil
  282. }
  283. return locale
  284. }
  285. }
  286. if !ok && i.ExtractFunc != nil {
  287. if v := i.ExtractFunc(ctx); v != "" {
  288. _, index, ok = i.TryMatchString(v)
  289. }
  290. }
  291. if !ok && i.URLParameter != "" {
  292. if v := ctx.URLParam(i.URLParameter); v != "" {
  293. _, index, ok = i.TryMatchString(v)
  294. }
  295. }
  296. if !ok && i.Cookie != "" {
  297. if v := ctx.GetCookie(i.Cookie); v != "" {
  298. _, index, ok = i.TryMatchString(v) // url.QueryUnescape(cookie.Value)
  299. }
  300. }
  301. if !ok && i.Subdomain {
  302. if v := ctx.Subdomain(); v != "" {
  303. _, index, ok = i.TryMatchString(v)
  304. }
  305. }
  306. if !ok {
  307. if v := ctx.GetHeader(acceptLanguageHeaderKey); v != "" {
  308. desired, _, err := language.ParseAcceptLanguage(v)
  309. if err == nil {
  310. if _, idx, conf := i.matcher.Match(desired...); conf > language.Low {
  311. index = idx
  312. }
  313. }
  314. }
  315. }
  316. // locale := i.localizer.GetLocale(index)
  317. // ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey(), locale)
  318. // // if 0 then it defaults to the first language.
  319. // return locale
  320. locale := i.localizer.GetLocale(index)
  321. if locale == nil {
  322. return nil
  323. }
  324. return locale
  325. }
  326. // GetMessage returns the localized text message for this "r" request based on the key "format".
  327. // It returns an empty string if locale or format not found.
  328. func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface{}) string {
  329. loc := i.GetLocale(ctx)
  330. if loc != nil {
  331. // it's not the default/fallback language and not message found for that lang:key.
  332. msg := loc.GetMessage(format, args...)
  333. if msg == "" && !i.Strict && loc.Index() > 0 {
  334. return i.localizer.GetLocale(0).GetMessage(format, args...)
  335. }
  336. }
  337. return ""
  338. }
  339. // Wrapper returns a new router wrapper.
  340. // The result function can be passed on `Application.WrapRouter`.
  341. // It compares the path prefix for translated language and
  342. // local redirects the requested path with the selected (from the path) language to the router.
  343. //
  344. // You do NOT have to call it manually, just set the `I18n.PathRedirect` field to true.
  345. func (i *I18n) Wrapper() router.WrapperFunc {
  346. if !i.PathRedirect {
  347. return nil
  348. }
  349. return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
  350. found := false
  351. path := r.URL.Path[1:]
  352. if idx := strings.IndexByte(path, '/'); idx > 0 {
  353. path = path[:idx]
  354. }
  355. if path != "" {
  356. if tag, _, ok := i.TryMatchString(path); ok {
  357. lang := tag.String()
  358. path = r.URL.Path[len(path)+1:]
  359. if path == "" {
  360. path = "/"
  361. }
  362. r.RequestURI = path
  363. r.URL.Path = path
  364. r.Header.Set(acceptLanguageHeaderKey, lang)
  365. found = true
  366. }
  367. }
  368. if !found && i.Subdomain {
  369. host := context.GetHost(r)
  370. if dotIdx := strings.IndexByte(host, '.'); dotIdx > 0 {
  371. if subdomain := host[0:dotIdx]; subdomain != "" {
  372. if tag, _, ok := i.TryMatchString(subdomain); ok {
  373. host = host[dotIdx+1:]
  374. r.URL.Host = host
  375. r.Host = host
  376. r.Header.Set(acceptLanguageHeaderKey, tag.String())
  377. }
  378. }
  379. }
  380. }
  381. next(w, r)
  382. }
  383. }