loader.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. package i18n
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "io/ioutil"
  7. "path/filepath"
  8. "strings"
  9. "text/template"
  10. "github.com/kataras/iris/context"
  11. "github.com/BurntSushi/toml"
  12. "golang.org/x/text/language"
  13. "gopkg.in/ini.v1"
  14. "gopkg.in/yaml.v3"
  15. )
  16. // LoaderConfig is an optional configuration structure which contains
  17. // some options about how the template loader should act.
  18. //
  19. // See `Glob` and `Assets` package-level functions.
  20. type LoaderConfig struct {
  21. // Template delimeters, defaults to {{ }}.
  22. Left, Right string
  23. // Template functions map, defaults to nil.
  24. FuncMap template.FuncMap
  25. // If true then it will return error on invalid templates instead of moving them to simple string-line keys.
  26. // Also it will report whether the registered languages matched the loaded ones.
  27. // Defaults to false.
  28. Strict bool
  29. }
  30. // LoaderOption is a type which accepts a pointer to `LoaderConfig`
  31. // and can be optionally passed to the second variadic input argument of the `Glob` and `Assets` functions.
  32. type LoaderOption func(*LoaderConfig)
  33. // Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob)
  34. // and loads the locale files based on any "options".
  35. //
  36. // The "globPattern" input parameter is a glob pattern which the default loader should
  37. // search and load for locale files.
  38. //
  39. // See `New` and `LoaderConfig` too.
  40. func Glob(globPattern string, options ...LoaderOption) Loader {
  41. assetNames, err := filepath.Glob(globPattern)
  42. if err != nil {
  43. panic(err)
  44. }
  45. return load(assetNames, ioutil.ReadFile, options...)
  46. }
  47. // Assets accepts a function that returns a list of filenames (physical or virtual),
  48. // another a function that should return the contents of a specific file
  49. // and any Loader options. Go-bindata usage.
  50. // It returns a valid `Loader` which loads and maps the locale files.
  51. //
  52. // See `Glob`, `Assets`, `New` and `LoaderConfig` too.
  53. func Assets(assetNames func() []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
  54. return load(assetNames(), asset, options...)
  55. }
  56. // load accepts a list of filenames (physical or virtual),
  57. // a function that should return the contents of a specific file
  58. // and any Loader options.
  59. // It returns a valid `Loader` which loads and maps the locale files.
  60. //
  61. // See `Glob`, `Assets` and `LoaderConfig` too.
  62. func load(assetNames []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
  63. var c = LoaderConfig{
  64. Left: "{{",
  65. Right: "}}",
  66. Strict: false,
  67. }
  68. for _, opt := range options {
  69. opt(&c)
  70. }
  71. return func(m *Matcher) (Localizer, error) {
  72. languageFiles, err := m.ParseLanguageFiles(assetNames)
  73. if err != nil {
  74. return nil, err
  75. }
  76. locales := make(MemoryLocalizer)
  77. for langIndex, langFiles := range languageFiles {
  78. keyValues := make(map[string]interface{})
  79. for _, fileName := range langFiles {
  80. unmarshal := yaml.Unmarshal
  81. if idx := strings.LastIndexByte(fileName, '.'); idx > 1 {
  82. switch fileName[idx:] {
  83. case ".toml", ".tml":
  84. unmarshal = toml.Unmarshal
  85. case ".json":
  86. unmarshal = json.Unmarshal
  87. case ".ini":
  88. unmarshal = unmarshalINI
  89. }
  90. }
  91. b, err := asset(fileName)
  92. if err != nil {
  93. return nil, err
  94. }
  95. if err = unmarshal(b, &keyValues); err != nil {
  96. return nil, err
  97. }
  98. }
  99. var (
  100. templateKeys = make(map[string]*template.Template)
  101. lineKeys = make(map[string]string)
  102. other = make(map[string]interface{})
  103. )
  104. for k, v := range keyValues {
  105. // fmt.Printf("[%d] %s = %v of type: [%T]\n", langIndex, k, v, v)
  106. switch value := v.(type) {
  107. case string:
  108. if leftIdx, rightIdx := strings.Index(value, c.Left), strings.Index(value, c.Right); leftIdx != -1 && rightIdx > leftIdx {
  109. // we assume it's template?
  110. if t, err := template.New(k).Delims(c.Left, c.Right).Funcs(c.FuncMap).Parse(value); err == nil {
  111. templateKeys[k] = t
  112. continue
  113. } else if c.Strict {
  114. return nil, err
  115. }
  116. }
  117. lineKeys[k] = value
  118. default:
  119. other[k] = v
  120. }
  121. }
  122. t := m.Languages[langIndex]
  123. locales[langIndex] = &defaultLocale{
  124. index: langIndex,
  125. id: t.String(),
  126. tag: &t,
  127. templateKeys: templateKeys,
  128. lineKeys: lineKeys,
  129. other: other,
  130. }
  131. }
  132. if n := len(locales); n == 0 {
  133. return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
  134. } else if c.Strict && n < len(m.Languages) {
  135. return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
  136. }
  137. return locales, nil
  138. }
  139. }
  140. // MemoryLocalizer is a map which implements the `Localizer`.
  141. type MemoryLocalizer map[int]context.Locale
  142. // GetLocale returns a valid `Locale` based on the "index".
  143. func (l MemoryLocalizer) GetLocale(index int) context.Locale {
  144. // loc, ok := l[index]
  145. // if !ok {
  146. // panic(fmt.Sprintf("locale of index [%d] not found", index))
  147. // }
  148. // return loc
  149. return l[index]
  150. }
  151. // SetDefault changes the default language based on the "index".
  152. // See `I18n#SetDefault` method for more.
  153. func (l MemoryLocalizer) SetDefault(index int) bool {
  154. // callers should protect with mutex if called at serve-time.
  155. if loc, ok := l[index]; ok {
  156. f := l[0]
  157. l[0] = loc
  158. l[index] = f
  159. return true
  160. }
  161. return false
  162. }
  163. type defaultLocale struct {
  164. index int
  165. id string
  166. tag *language.Tag
  167. // templates *template.Template // we could use the ExecuteTemplate too.
  168. templateKeys map[string]*template.Template
  169. lineKeys map[string]string
  170. other map[string]interface{}
  171. }
  172. func (l *defaultLocale) Index() int {
  173. return l.index
  174. }
  175. func (l *defaultLocale) Tag() *language.Tag {
  176. return l.tag
  177. }
  178. func (l *defaultLocale) Language() string {
  179. return l.id
  180. }
  181. func (l *defaultLocale) GetMessage(key string, args ...interface{}) string {
  182. n := len(args)
  183. if n > 0 {
  184. // search on templates.
  185. if tmpl, ok := l.templateKeys[key]; ok {
  186. buf := new(bytes.Buffer)
  187. if err := tmpl.Execute(buf, args[0]); err == nil {
  188. return buf.String()
  189. }
  190. }
  191. }
  192. if text, ok := l.lineKeys[key]; ok {
  193. return fmt.Sprintf(text, args...)
  194. }
  195. if v, ok := l.other[key]; ok {
  196. if n > 0 {
  197. return fmt.Sprintf("%v [%v]", v, args)
  198. }
  199. return fmt.Sprintf("%v", v)
  200. }
  201. return ""
  202. }
  203. func unmarshalINI(data []byte, v interface{}) error {
  204. f, err := ini.Load(data)
  205. if err != nil {
  206. return err
  207. }
  208. m := *v.(*map[string]interface{})
  209. // Includes the ini.DefaultSection which has the root keys too.
  210. // We don't have to iterate to each section to find the subsection,
  211. // the Sections() returns all sections, sub-sections are separated by dot '.'
  212. // and we match the dot with a section on the translate function, so we just save the values as they are,
  213. // so we don't have to do section lookup on every translate call.
  214. for _, section := range f.Sections() {
  215. keyPrefix := ""
  216. if name := section.Name(); name != ini.DefaultSection {
  217. keyPrefix = name + "."
  218. }
  219. for _, key := range section.Keys() {
  220. m[keyPrefix+key.Name()] = key.Value()
  221. }
  222. }
  223. return nil
  224. }