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 }