loader.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. package i18n
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "io/fs"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "github.com/kataras/iris/v12/i18n/internal"
  11. "github.com/BurntSushi/toml"
  12. "gopkg.in/ini.v1"
  13. "gopkg.in/yaml.v3"
  14. )
  15. // LoaderConfig the configuration structure which contains
  16. // some options about how the template loader should act.
  17. //
  18. // See `Glob` and `Assets` package-level functions.
  19. type LoaderConfig = internal.Options
  20. // Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob)
  21. // and loads the locale files based on any "options".
  22. //
  23. // The "globPattern" input parameter is a glob pattern which the default loader should
  24. // search and load for locale files.
  25. //
  26. // See `New` and `LoaderConfig` too.
  27. func Glob(globPattern string, options LoaderConfig) Loader {
  28. assetNames, err := filepath.Glob(globPattern)
  29. if err != nil {
  30. panic(err)
  31. }
  32. return load(assetNames, os.ReadFile, options)
  33. }
  34. // Assets accepts a function that returns a list of filenames (physical or virtual),
  35. // another a function that should return the contents of a specific file
  36. // and any Loader options. Go-bindata usage.
  37. // It returns a valid `Loader` which loads and maps the locale files.
  38. //
  39. // See `Glob`, `FS`, `New` and `LoaderConfig` too.
  40. func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
  41. return load(assetNames(), asset, options)
  42. }
  43. // LoadFS loads the files using embed.FS or fs.FS or
  44. // http.FileSystem or string (local directory).
  45. // The "pattern" is a classic glob pattern.
  46. //
  47. // See `Glob`, `Assets`, `New` and `LoaderConfig` too.
  48. func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) {
  49. pattern = strings.TrimPrefix(pattern, "./")
  50. assetNames, err := fs.Glob(fileSystem, pattern)
  51. if err != nil {
  52. return nil, err
  53. }
  54. assetFunc := func(name string) ([]byte, error) {
  55. f, err := fileSystem.Open(name)
  56. if err != nil {
  57. return nil, err
  58. }
  59. return io.ReadAll(f)
  60. }
  61. return load(assetNames, assetFunc, options), nil
  62. }
  63. // LangMap key as language (e.g. "el-GR") and value as a map of key-value pairs (e.g. "hello": "Γειά").
  64. type LangMap = map[string]map[string]interface{}
  65. // KV is a loader which accepts a map of language(key) and the available key-value pairs.
  66. // Example Code:
  67. //
  68. // m := i18n.LangMap{
  69. // "en-US": map[string]interface{}{
  70. // "hello": "Hello",
  71. // },
  72. // "el-GR": map[string]interface{}{
  73. // "hello": "Γειά",
  74. // },
  75. // }
  76. //
  77. // app := iris.New()
  78. // [...]
  79. // app.I18N.LoadKV(m)
  80. // app.I18N.SetDefault("en-US")
  81. func KV(langMap LangMap, opts ...LoaderConfig) Loader {
  82. return func(m *Matcher) (Localizer, error) {
  83. options := DefaultLoaderConfig
  84. if len(opts) > 0 {
  85. options = opts[0]
  86. }
  87. languageIndexes := make([]int, 0, len(langMap))
  88. keyValuesMulti := make([]map[string]interface{}, 0, len(langMap))
  89. for languageName, pairs := range langMap {
  90. langIndex := parseLanguageName(m, languageName) // matches and adds the language tag to m.Languages.
  91. languageIndexes = append(languageIndexes, langIndex)
  92. keyValuesMulti = append(keyValuesMulti, pairs)
  93. }
  94. cat, err := internal.NewCatalog(m.Languages, options)
  95. if err != nil {
  96. return nil, err
  97. }
  98. for _, langIndex := range languageIndexes {
  99. if langIndex == -1 {
  100. // If loader has more languages than defined for use in New function,
  101. // e.g. when New(KV(m), "en-US") contains el-GR and en-US but only "en-US" passed.
  102. continue
  103. }
  104. kv := keyValuesMulti[langIndex]
  105. err := cat.Store(langIndex, kv)
  106. if err != nil {
  107. return nil, err
  108. }
  109. }
  110. if n := len(cat.Locales); n == 0 {
  111. return nil, fmt.Errorf("locales not found in map")
  112. } else if options.Strict && n < len(m.Languages) {
  113. return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
  114. }
  115. return cat, nil
  116. }
  117. }
  118. // DefaultLoaderConfig represents the default loader configuration.
  119. var DefaultLoaderConfig = LoaderConfig{
  120. Left: "{{",
  121. Right: "}}",
  122. Strict: false,
  123. DefaultMessageFunc: nil,
  124. PluralFormDecoder: internal.DefaultPluralFormDecoder,
  125. Funcs: nil,
  126. }
  127. // load accepts a list of filenames (physical or virtual),
  128. // a function that should return the contents of a specific file
  129. // and any Loader options.
  130. // It returns a valid `Loader` which loads and maps the locale files.
  131. //
  132. // See `FS`, `Glob`, `Assets` and `LoaderConfig` too.
  133. func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
  134. return func(m *Matcher) (Localizer, error) {
  135. languageFiles, err := m.ParseLanguageFiles(assetNames)
  136. if err != nil {
  137. return nil, err
  138. }
  139. if options.DefaultMessageFunc == nil {
  140. options.DefaultMessageFunc = m.defaultMessageFunc
  141. }
  142. cat, err := internal.NewCatalog(m.Languages, options)
  143. if err != nil {
  144. return nil, err
  145. }
  146. for langIndex, langFiles := range languageFiles {
  147. keyValues := make(map[string]interface{})
  148. for _, fileName := range langFiles {
  149. unmarshal := yaml.Unmarshal
  150. if idx := strings.LastIndexByte(fileName, '.'); idx > 1 {
  151. switch fileName[idx:] {
  152. case ".toml", ".tml":
  153. unmarshal = toml.Unmarshal
  154. case ".json":
  155. unmarshal = json.Unmarshal
  156. case ".ini":
  157. unmarshal = unmarshalINI
  158. }
  159. }
  160. b, err := asset(fileName)
  161. if err != nil {
  162. return nil, err
  163. }
  164. if err = unmarshal(b, &keyValues); err != nil {
  165. return nil, err
  166. }
  167. }
  168. err = cat.Store(langIndex, keyValues)
  169. if err != nil {
  170. return nil, err
  171. }
  172. }
  173. if n := len(cat.Locales); n == 0 {
  174. return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
  175. } else if options.Strict && n < len(m.Languages) {
  176. return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
  177. }
  178. return cat, nil
  179. }
  180. }
  181. func unmarshalINI(data []byte, v interface{}) error {
  182. f, err := ini.Load(data)
  183. if err != nil {
  184. return err
  185. }
  186. m := *v.(*map[string]interface{})
  187. // Includes the ini.DefaultSection which has the root keys too.
  188. // We don't have to iterate to each section to find the subsection,
  189. // the Sections() returns all sections, sub-sections are separated by dot '.'
  190. // and we match the dot with a section on the translate function, so we just save the values as they are,
  191. // so we don't have to do section lookup on every translate call.
  192. for _, section := range f.Sections() {
  193. keyPrefix := ""
  194. if name := section.Name(); name != ini.DefaultSection {
  195. keyPrefix = name + "."
  196. }
  197. for _, key := range section.Keys() {
  198. m[keyPrefix+key.Name()] = key.Value()
  199. }
  200. }
  201. return nil
  202. }