From 0954986a66a0feb8ab5850c80dcdd7b37fc01efd Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 18 Mar 2023 15:43:18 +0200 Subject: [PATCH] add LoadKV method on Iris.Application.I18N instance --- HISTORY.md | 2 + .../embedded/locales/el-GR/other.ini | 4 +- .../embedded/locales/el-GR/user.ini | 2 +- .../embedded/locales/en-US/other.ini | 4 +- .../embedded/locales/en-US/user.ini | 2 +- _examples/i18n/template-embedded/main.go | 17 ++++- i18n/i18n.go | 21 +++++- i18n/internal/catalog.go | 1 + i18n/loader.go | 66 ++++++++++++++++++- 9 files changed, 108 insertions(+), 11 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 11405bbd3e..21cfeac959 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -23,6 +23,8 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene Change applies to `master` branch. +- Add `LoadKV` method on `Iris.Application.I18N` instance. It should be used when no locale files are available. It loads locales via pure Go Map (or database decoded values). + - Remove [ace](https://github.com/eknkc/amber) template parser support, as it was discontinued by its author more than five years ago. # Sa, 11 March 2023 | v12.2.0 diff --git a/_examples/i18n/template-embedded/embedded/locales/el-GR/other.ini b/_examples/i18n/template-embedded/embedded/locales/el-GR/other.ini index 5954cdd355..8ebc6727c5 100644 --- a/_examples/i18n/template-embedded/embedded/locales/el-GR/other.ini +++ b/_examples/i18n/template-embedded/embedded/locales/el-GR/other.ini @@ -4,7 +4,7 @@ User = Λογαριασμός [debug] Title = Μενού προγραμματιστή AccessLog = Πρόσβαση στο αρχείο καταγραφής -AccessLogClear = Καθαρισμός {{tr "debug.AccessLog"}} +AccessLogClear = Καθαρισμός {{ tr "debug.AccessLog" }} [user.connections] -Title = {{tr "nav.User"}} Συνδέσεις \ No newline at end of file +Title = {{ tr "nav.User" }} Συνδέσεις \ No newline at end of file diff --git a/_examples/i18n/template-embedded/embedded/locales/el-GR/user.ini b/_examples/i18n/template-embedded/embedded/locales/el-GR/user.ini index 0a110cdc60..3a6c6409dc 100644 --- a/_examples/i18n/template-embedded/embedded/locales/el-GR/user.ini +++ b/_examples/i18n/template-embedded/embedded/locales/el-GR/user.ini @@ -1,4 +1,4 @@ [forms] member = μέλος -register = Γίνε {{uppercase (tr "forms.member") }} +register = Γίνε {{ uppercase (tr "forms.member") }} registered = εγγεγραμμένοι \ No newline at end of file diff --git a/_examples/i18n/template-embedded/embedded/locales/en-US/other.ini b/_examples/i18n/template-embedded/embedded/locales/en-US/other.ini index 31f1b18266..f8c753365a 100644 --- a/_examples/i18n/template-embedded/embedded/locales/en-US/other.ini +++ b/_examples/i18n/template-embedded/embedded/locales/en-US/other.ini @@ -6,7 +6,7 @@ User = Account [debug] Title = Developer Menu AccessLog = Access Log -AccessLogClear = Clear {{tr "debug.AccessLog"}} +AccessLogClear = Clear {{ tr "debug.AccessLog" }} [user.connections] -Title = {{tr "nav.User"}} Connections \ No newline at end of file +Title = {{ tr "nav.User" }} Connections \ No newline at end of file diff --git a/_examples/i18n/template-embedded/embedded/locales/en-US/user.ini b/_examples/i18n/template-embedded/embedded/locales/en-US/user.ini index 09d3e3e4ab..2498d029c7 100644 --- a/_examples/i18n/template-embedded/embedded/locales/en-US/user.ini +++ b/_examples/i18n/template-embedded/embedded/locales/en-US/user.ini @@ -1,4 +1,4 @@ [forms] member = member -register = Become a {{uppercase (tr "forms.member") }} +register = Become a {{ uppercase (tr "forms.member") }} registered = registered \ No newline at end of file diff --git a/_examples/i18n/template-embedded/main.go b/_examples/i18n/template-embedded/main.go index bf8f83598c..ff142ace23 100644 --- a/_examples/i18n/template-embedded/main.go +++ b/_examples/i18n/template-embedded/main.go @@ -13,6 +13,16 @@ var embeddedFS embed.FS func main() { app := newApp() + // http://localhost:8080 + // http://localhost:8080?lang=el + // http://localhost:8080?lang=el + // http://localhost:8080?lang=el-GR + // http://localhost:8080?lang=en + // http://localhost:8080?lang=en-US + // + // http://localhost:8080/title + // http://localhost:8080/title?lang=el-GR + // ... app.Listen(":8080") } @@ -30,11 +40,14 @@ func newApp() *iris.Application { // Instead of: // err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR") - // Apply the below in order to build with embedded locales inside your executable binary. + // apply the below in order to build with embedded locales inside your executable binary. err := app.I18n.LoadFS(embeddedFS, "./embedded/locales/*/*.ini", "en-US", "el-GR") if err != nil { panic(err) - } + } // OR to load all languages by filename: + // app.I18n.LoadFS(embeddedFS, "./embedded/locales/*/*.ini") + // Then set the default language using: + // app.I18n.SetDefault("en-US") app.Get("/", func(ctx iris.Context) { text := ctx.Tr("forms.register") // en-US: prints "Become a MEMBER". diff --git a/i18n/i18n.go b/i18n/i18n.go index 514aec48c3..b42a9867f0 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -141,8 +141,8 @@ func (i *I18n) LoadAssets(assetNames func() []string, asset func(string) ([]byte return i.Reset(Assets(assetNames, asset, i.Loader), languages...) } -// LoadFS is a method shortcut to load files using `embed.FS` or `fs.FS` or -// `http.FileSystem` or `string` (local directory). +// LoadFS is a method shortcut to load files using +// an `embed.FS` or `fs.FS` or `http.FileSystem` value. // The "pattern" is a classic glob pattern. // // See `New` and `FS` package-level functions for more. @@ -156,6 +156,13 @@ func (i *I18n) LoadFS(fileSystem fs.FS, pattern string, languages ...string) err return i.Reset(loader, languages...) } +// LoadKV is a method shortcut to load locales from a map of specified languages. +// See `KV` package-level function for more. +func (i *I18n) LoadKV(langMap LangMap, languages ...string) error { + loader := KV(langMap, i.Loader) + return i.Reset(loader, languages...) +} + // Reset sets the locales loader and languages. // It is not meant to be used by users unless // a custom `Loader` must be used instead of the default one. @@ -300,6 +307,16 @@ func parsePath(m *Matcher, path string) int { return -1 } +func parseLanguageName(m *Matcher, name string) int { + if t, err := language.Parse(name); err == nil { + if _, index, conf := m.MatchOrAdd(t); conf > language.Low { + return index + } + } + + return -1 +} + func reverseStrings(s []string) []string { for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { s[i], s[j] = s[j], s[i] diff --git a/i18n/internal/catalog.go b/i18n/internal/catalog.go index 01e12b17a0..26f551fc36 100644 --- a/i18n/internal/catalog.go +++ b/i18n/internal/catalog.go @@ -5,6 +5,7 @@ import ( "text/template" "github.com/kataras/iris/v12/context" + "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/message/catalog" diff --git a/i18n/loader.go b/i18n/loader.go index e418e22b8b..081e5e2868 100644 --- a/i18n/loader.go +++ b/i18n/loader.go @@ -73,6 +73,70 @@ func FS(fileSystem fs.FS, pattern string, options LoaderConfig) (Loader, error) 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: "{{", @@ -88,7 +152,7 @@ var DefaultLoaderConfig = LoaderConfig{ // and any Loader options. // It returns a valid `Loader` which loads and maps the locale files. // -// See `Glob`, `Assets` and `LoaderConfig` too. +// 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)