123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- package i18n
- import (
- "encoding/json"
- "fmt"
- "io"
- "io/fs"
- "os"
- "path/filepath"
- "strings"
- "github.com/kataras/iris/v12/i18n/internal"
- "github.com/BurntSushi/toml"
- "gopkg.in/ini.v1"
- "gopkg.in/yaml.v3"
- )
- // LoaderConfig the configuration structure which contains
- // some options about how the template loader should act.
- //
- // See `Glob` and `Assets` package-level functions.
- type LoaderConfig = internal.Options
- // Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob)
- // and loads the locale files based on any "options".
- //
- // The "globPattern" input parameter is a glob pattern which the default loader should
- // search and load for locale files.
- //
- // See `New` and `LoaderConfig` too.
- func Glob(globPattern string, options LoaderConfig) Loader {
- assetNames, err := filepath.Glob(globPattern)
- if err != nil {
- panic(err)
- }
- return load(assetNames, os.ReadFile, options)
- }
- // Assets accepts a function that returns a list of filenames (physical or virtual),
- // another a function that should return the contents of a specific file
- // and any Loader options. Go-bindata usage.
- // It returns a valid `Loader` which loads and maps the locale files.
- //
- // See `Glob`, `FS`, `New` and `LoaderConfig` too.
- func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
- return load(assetNames(), asset, options)
- }
- // LoadFS loads the files using embed.FS or fs.FS or
- // http.FileSystem or string (local directory).
- // The "pattern" is a classic glob pattern.
- //
- // See `Glob`, `Assets`, `New` and `LoaderConfig` too.
- func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) {
- pattern = strings.TrimPrefix(pattern, "./")
- assetNames, err := fs.Glob(fileSystem, pattern)
- if err != nil {
- return nil, err
- }
- assetFunc := func(name string) ([]byte, error) {
- f, err := fileSystem.Open(name)
- if err != nil {
- return nil, err
- }
- return io.ReadAll(f)
- }
- return load(assetNames, assetFunc, options), nil
- }
- // LangMap key as language (e.g. "el-GR") and value as a map of key-value pairs (e.g. "hello": "Γειά").
- type LangMap = map[string]map[string]interface{}
- // KV is a loader which accepts a map of language(key) and the available key-value pairs.
- // Example Code:
- //
- // m := i18n.LangMap{
- // "en-US": map[string]interface{}{
- // "hello": "Hello",
- // },
- // "el-GR": map[string]interface{}{
- // "hello": "Γειά",
- // },
- // }
- //
- // app := iris.New()
- // [...]
- // app.I18N.LoadKV(m)
- // app.I18N.SetDefault("en-US")
- func KV(langMap LangMap, opts ...LoaderConfig) Loader {
- return func(m *Matcher) (Localizer, error) {
- options := DefaultLoaderConfig
- if len(opts) > 0 {
- options = opts[0]
- }
- languageIndexes := make([]int, 0, len(langMap))
- keyValuesMulti := make([]map[string]interface{}, 0, len(langMap))
- for languageName, pairs := range langMap {
- langIndex := parseLanguageName(m, languageName) // matches and adds the language tag to m.Languages.
- languageIndexes = append(languageIndexes, langIndex)
- keyValuesMulti = append(keyValuesMulti, pairs)
- }
- cat, err := internal.NewCatalog(m.Languages, options)
- if err != nil {
- return nil, err
- }
- for _, langIndex := range languageIndexes {
- if langIndex == -1 {
- // If loader has more languages than defined for use in New function,
- // e.g. when New(KV(m), "en-US") contains el-GR and en-US but only "en-US" passed.
- continue
- }
- kv := keyValuesMulti[langIndex]
- err := cat.Store(langIndex, kv)
- if err != nil {
- return nil, err
- }
- }
- if n := len(cat.Locales); n == 0 {
- return nil, fmt.Errorf("locales not found in map")
- } else if options.Strict && n < len(m.Languages) {
- return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
- }
- return cat, nil
- }
- }
- // DefaultLoaderConfig represents the default loader configuration.
- var DefaultLoaderConfig = LoaderConfig{
- Left: "{{",
- Right: "}}",
- Strict: false,
- DefaultMessageFunc: nil,
- PluralFormDecoder: internal.DefaultPluralFormDecoder,
- Funcs: nil,
- }
- // load accepts a list of filenames (physical or virtual),
- // a function that should return the contents of a specific file
- // and any Loader options.
- // It returns a valid `Loader` which loads and maps the locale files.
- //
- // See `FS`, `Glob`, `Assets` and `LoaderConfig` too.
- func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
- return func(m *Matcher) (Localizer, error) {
- languageFiles, err := m.ParseLanguageFiles(assetNames)
- if err != nil {
- return nil, err
- }
- if options.DefaultMessageFunc == nil {
- options.DefaultMessageFunc = m.defaultMessageFunc
- }
- cat, err := internal.NewCatalog(m.Languages, options)
- if err != nil {
- return nil, err
- }
- for langIndex, langFiles := range languageFiles {
- keyValues := make(map[string]interface{})
- for _, fileName := range langFiles {
- unmarshal := yaml.Unmarshal
- if idx := strings.LastIndexByte(fileName, '.'); idx > 1 {
- switch fileName[idx:] {
- case ".toml", ".tml":
- unmarshal = toml.Unmarshal
- case ".json":
- unmarshal = json.Unmarshal
- case ".ini":
- unmarshal = unmarshalINI
- }
- }
- b, err := asset(fileName)
- if err != nil {
- return nil, err
- }
- if err = unmarshal(b, &keyValues); err != nil {
- return nil, err
- }
- }
- err = cat.Store(langIndex, keyValues)
- if err != nil {
- return nil, err
- }
- }
- if n := len(cat.Locales); n == 0 {
- return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
- } else if options.Strict && n < len(m.Languages) {
- return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
- }
- return cat, nil
- }
- }
- func unmarshalINI(data []byte, v interface{}) error {
- f, err := ini.Load(data)
- if err != nil {
- return err
- }
- m := *v.(*map[string]interface{})
- // Includes the ini.DefaultSection which has the root keys too.
- // We don't have to iterate to each section to find the subsection,
- // the Sections() returns all sections, sub-sections are separated by dot '.'
- // and we match the dot with a section on the translate function, so we just save the values as they are,
- // so we don't have to do section lookup on every translate call.
- for _, section := range f.Sections() {
- keyPrefix := ""
- if name := section.Name(); name != ini.DefaultSection {
- keyPrefix = name + "."
- }
- for _, key := range section.Keys() {
- m[keyPrefix+key.Name()] = key.Value()
- }
- }
- return nil
- }
|