taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

loader.go (6657B)


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