2019-12-13 22:06:18 +01:00
|
|
|
package i18n
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2022-09-23 00:28:47 +02:00
|
|
|
"io"
|
2022-09-25 19:40:56 +02:00
|
|
|
"io/fs"
|
2022-06-17 21:03:18 +02:00
|
|
|
"os"
|
2019-12-13 22:06:18 +01:00
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
"github.com/kataras/iris/v12/i18n/internal"
|
2019-12-13 22:06:18 +01:00
|
|
|
|
|
|
|
"github.com/BurntSushi/toml"
|
|
|
|
"gopkg.in/ini.v1"
|
2020-01-07 02:41:07 +01:00
|
|
|
"gopkg.in/yaml.v3"
|
2019-12-13 22:06:18 +01:00
|
|
|
)
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
// LoaderConfig the configuration structure which contains
|
2019-12-13 22:06:18 +01:00
|
|
|
// some options about how the template loader should act.
|
|
|
|
//
|
|
|
|
// See `Glob` and `Assets` package-level functions.
|
2020-09-29 18:19:19 +02:00
|
|
|
type LoaderConfig = internal.Options
|
2019-12-13 22:06:18 +01:00
|
|
|
|
|
|
|
// 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.
|
2020-09-29 18:19:19 +02:00
|
|
|
func Glob(globPattern string, options LoaderConfig) Loader {
|
2019-12-13 22:06:18 +01:00
|
|
|
assetNames, err := filepath.Glob(globPattern)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2022-06-17 21:03:18 +02:00
|
|
|
return load(assetNames, os.ReadFile, options)
|
2019-12-13 22:06:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
//
|
2022-09-23 00:28:47 +02:00
|
|
|
// See `Glob`, `FS`, `New` and `LoaderConfig` too.
|
2020-09-29 18:19:19 +02:00
|
|
|
func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
|
|
|
|
return load(assetNames(), asset, options)
|
|
|
|
}
|
|
|
|
|
2022-09-23 00:28:47 +02:00
|
|
|
// LoadFS loads the files using embed.FS or fs.FS or
|
|
|
|
// http.FileSystem or string (local directory).
|
2022-09-25 19:40:56 +02:00
|
|
|
// The "pattern" is a classic glob pattern.
|
2022-09-23 00:28:47 +02:00
|
|
|
//
|
|
|
|
// See `Glob`, `Assets`, `New` and `LoaderConfig` too.
|
2022-09-25 19:40:56 +02:00
|
|
|
func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) {
|
|
|
|
pattern = strings.TrimPrefix(pattern, "./")
|
2022-09-23 00:28:47 +02:00
|
|
|
|
2022-09-25 19:40:56 +02:00
|
|
|
assetNames, err := fs.Glob(fileSystem, pattern)
|
2022-09-23 00:28:47 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-03-18 14:43:18 +01:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
// DefaultLoaderConfig represents the default loader configuration.
|
|
|
|
var DefaultLoaderConfig = LoaderConfig{
|
|
|
|
Left: "{{",
|
|
|
|
Right: "}}",
|
|
|
|
Strict: false,
|
|
|
|
DefaultMessageFunc: nil,
|
|
|
|
PluralFormDecoder: internal.DefaultPluralFormDecoder,
|
|
|
|
Funcs: nil,
|
2019-12-13 22:06:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
//
|
2023-03-18 14:43:18 +01:00
|
|
|
// See `FS`, `Glob`, `Assets` and `LoaderConfig` too.
|
2020-09-29 18:19:19 +02:00
|
|
|
func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
|
2019-12-13 22:06:18 +01:00
|
|
|
return func(m *Matcher) (Localizer, error) {
|
|
|
|
languageFiles, err := m.ParseLanguageFiles(assetNames)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
if options.DefaultMessageFunc == nil {
|
|
|
|
options.DefaultMessageFunc = m.defaultMessageFunc
|
|
|
|
}
|
|
|
|
|
|
|
|
cat, err := internal.NewCatalog(m.Languages, options)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-12-13 22:06:18 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
err = cat.Store(langIndex, keyValues)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2019-12-13 22:06:18 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
if n := len(cat.Locales); n == 0 {
|
2019-12-13 22:06:18 +01:00
|
|
|
return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
|
2020-09-29 18:19:19 +02:00
|
|
|
} else if options.Strict && n < len(m.Languages) {
|
2019-12-13 22:06:18 +01:00
|
|
|
return nil, fmt.Errorf("locales expected to be %d but %d parsed", len(m.Languages), n)
|
|
|
|
}
|
|
|
|
|
2020-09-29 18:19:19 +02:00
|
|
|
return cat, nil
|
2020-08-16 17:04:52 +02:00
|
|
|
}
|
2019-12-13 22:06:18 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|