add support for the go standard embed tag for locale files

This commit is contained in:
Gerasimos (Makis) Maropoulos 2022-09-23 01:28:47 +03:00
parent fd1db640a0
commit 4cd0621018
No known key found for this signature in database
GPG Key ID: 403EEB7885C79503
14 changed files with 230 additions and 57 deletions

View File

@ -28,6 +28,7 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
- Support of embedded [locale files](https://github.com/kataras/iris/blob/master/_examples/i18n/template-embedded/main.go) using standard `embed.FS` with the new `LoadFS` function.
- Support of direct embedded view templates with `embed.FS` or `fs.FS` (in addition to `string` and `http.FileSystem` types).
- Add `iris.Patches()` package-level function to customize Iris Request Context REST (and more to come) behavior.

View File

@ -218,6 +218,7 @@
* Localization and Internationalization
* [Basic](i18n/basic)
* [Ttemplates and Functions](i18n/template)
* [Ttemplates and Functions (Embedded)](i18n/template-embedded)
* [Pluralization and Variables](i18n/plurals)
* Authentication, Authorization & Bot Detection
* [Recommended: Auth package and Single-Sign-On](auth/auth) **NEW (GO 1.18 Generics required)**

View File

@ -0,0 +1,10 @@
[nav]
User = Λογαριασμός
[debug]
Title = Μενού προγραμματιστή
AccessLog = Πρόσβαση στο αρχείο καταγραφής
AccessLogClear = Καθαρισμός {{tr "debug.AccessLog"}}
[user.connections]
Title = {{tr "nav.User"}} Συνδέσεις

View File

@ -0,0 +1,4 @@
[forms]
member = μέλος
register = Γίνε {{uppercase (tr "forms.member") }}
registered = εγγεγραμμένοι

View File

@ -0,0 +1,12 @@
# just an example of some more nested keys,
# see /other endpoint.
[nav]
User = Account
[debug]
Title = Developer Menu
AccessLog = Access Log
AccessLogClear = Clear {{tr "debug.AccessLog"}}
[user.connections]
Title = {{tr "nav.User"}} Connections

View File

@ -0,0 +1,4 @@
[forms]
member = member
register = Become a {{uppercase (tr "forms.member") }}
registered = registered

View File

@ -0,0 +1,50 @@
package main
import (
"embed"
"strings"
"text/template"
"github.com/kataras/iris/v12"
)
//go:embed locales/*
var filesystem embed.FS
func main() {
app := newApp()
app.Listen(":8080")
}
func newApp() *iris.Application {
app := iris.New()
// Set custom functions per locale!
app.I18n.Loader.Funcs = func(current iris.Locale) template.FuncMap {
return template.FuncMap{
"uppercase": func(word string) string {
return strings.ToUpper(word)
},
}
}
// 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.
err := app.I18n.LoadFS(filesystem, ".", "en-US", "el-GR")
if err != nil {
panic(err)
}
app.Get("/", func(ctx iris.Context) {
text := ctx.Tr("forms.register") // en-US: prints "Become a MEMBER".
ctx.WriteString(text)
})
app.Get("/title", func(ctx iris.Context) {
text := ctx.Tr("user.connections.Title") // en-US: prints "Accounts Connections".
ctx.WriteString(text)
})
return app
}

View File

@ -0,0 +1,21 @@
package main
import (
"testing"
"github.com/kataras/iris/v12/httptest"
)
func TestI18nLoaderFuncMap(t *testing.T) {
app := newApp()
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
Body().Equal("Become a MEMBER")
e.GET("/title").Expect().Status(httptest.StatusOK).
Body().Equal("Account Connections")
e.GET("/").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK).
Body().Equal("Γίνε ΜΈΛΟΣ")
e.GET("/title").WithHeader("Accept-Language", "el").Expect().Status(httptest.StatusOK).
Body().Equal("Λογαριασμός Συνδέσεις")
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/fs"
"net/http"
"path"
)
// ResolveFS accepts a single input argument of any type
@ -44,3 +45,67 @@ var ResolveFS = func(fsOrDir interface{}) http.FileSystem {
return fileSystem
}
// FindNames accepts a "http.FileSystem" and a root name and returns
// the list containg its file names.
func FindNames(fileSystem http.FileSystem, name string) ([]string, error) {
f, err := fileSystem.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
if !fi.IsDir() {
return []string{name}, nil
}
fileinfos, err := f.Readdir(-1)
if err != nil {
return nil, err
}
files := make([]string, 0)
for _, info := range fileinfos {
// Note:
// go-bindata has absolute names with os.Separator,
// http.Dir the basename.
filename := toBaseName(info.Name())
fullname := path.Join(name, filename)
if fullname == name { // prevent looping through itself.
continue
}
rfiles, err := FindNames(fileSystem, fullname)
if err != nil {
return nil, err
}
files = append(files, rfiles...)
}
return files, nil
}
// Instead of path.Base(filepath.ToSlash(s))
// let's do something like that, it is faster
// (used to list directories on serve-time too):
func toBaseName(s string) string {
n := len(s) - 1
for i := n; i >= 0; i-- {
if c := s[i]; c == '/' || c == '\\' {
if i == n {
// "s" ends with a slash, remove it and retry.
return toBaseName(s[:n])
}
return s[i+1:] // return the rest, trimming the slash.
}
}
return s
}

View File

@ -372,7 +372,7 @@ func FileServer(fs http.FileSystem, options DirOptions) context.Handler {
}
prefixURL := strings.TrimSuffix(r.RequestURI, name)
names, err := findNames(fs, name)
names, err := context.FindNames(fs, name)
if err == nil {
for _, indexAsset := range names {
// it's an index file, do not pushed that.
@ -858,7 +858,7 @@ func fsOpener(fs http.FileSystem, options DirCacheOptions) func(name string, r *
func cache(fs http.FileSystem, options DirCacheOptions) (*cacheFS, error) {
start := time.Now()
names, err := findNames(fs, "/")
names, err := context.FindNames(fs, "/")
if err != nil {
return nil, err
}
@ -1175,49 +1175,6 @@ func (f *file) Get(alg string) (http.File, error) {
return f.Get("")
}
func findNames(fs http.FileSystem, name string) ([]string, error) {
f, err := fs.Open(name)
if err != nil {
return nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, err
}
if !fi.IsDir() {
return []string{name}, nil
}
fileinfos, err := f.Readdir(-1)
if err != nil {
return nil, err
}
files := make([]string, 0)
for _, info := range fileinfos {
// Note:
// go-bindata has absolute names with os.Separator,
// http.Dir the basename.
filename := toBaseName(info.Name())
fullname := path.Join(name, filename)
if fullname == name { // prevent looping through itself when fs is cacheFS.
continue
}
rfiles, err := findNames(fs, fullname)
if err != nil {
return nil, err
}
files = append(files, rfiles...)
}
return files, nil
}
type fileInfo struct {
baseName string
modTime time.Time

8
go.mod
View File

@ -2,7 +2,7 @@ module github.com/kataras/iris/v12
go 1.19
// retract v12.1.8 // please update to @master
retract v12.1.8 // Please update to @master
require (
github.com/BurntSushi/toml v1.2.0
@ -40,11 +40,11 @@ require (
github.com/vmihailenco/msgpack/v5 v5.3.5
github.com/yosssi/ace v0.0.5
go.etcd.io/bbolt v1.3.6
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0
golang.org/x/net v0.0.0-20220921203646-d300de134e69
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8
golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af
google.golang.org/protobuf v1.28.1
gopkg.in/ini.v1 v1.67.0
gopkg.in/yaml.v3 v3.0.1

12
go.sum generated
View File

@ -245,13 +245,13 @@ go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0 h1:a5Yg6ylndHHYJqIPrdq0AhvR6KTvDTAvgBtaidhEevY=
golang.org/x/crypto v0.0.0-20220919173607-35f4265a4bc0/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/net v0.0.0-20190327091125-710a502c58a2/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591 h1:D0B/7al0LLrVC8aWF4+oxpv/m8bc7ViFfVS8/gXGdqI=
golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20220921203646-d300de134e69 h1:hUJpGDpnfwdJW8iNypFjmSY0sCBEL+spFTZ2eO+Sfps=
golang.org/x/net v0.0.0-20220921203646-d300de134e69/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -274,8 +274,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y=
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -140,6 +140,21 @@ 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).
// With this method, all the embedded files into "sub" MUST be locale files.
//
// See `New` and `FS` package-level functions for more.
// Example: https://github.com/kataras/iris/blob/master/_examples/i18n/template-embedded/main.go.
func (i *I18n) LoadFS(fsOrDir interface{}, sub string, languages ...string) error {
loader, err := FS(fsOrDir, sub, i.Loader)
if err != nil {
return err
}
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.
@ -474,7 +489,9 @@ func (i *I18n) setLangWithoutContext(w http.ResponseWriter, r *http.Request, lan
SameSite: http.SameSiteLaxMode,
})
} else if i.URLParameter != "" {
r.URL.Query().Set(i.URLParameter, lang)
q := r.URL.Query()
q.Set(i.URLParameter, lang)
r.URL.RawQuery = q.Encode()
}
r.Header.Set(acceptLanguageHeaderKey, lang)

View File

@ -3,10 +3,12 @@ package i18n
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/i18n/internal"
"github.com/BurntSushi/toml"
@ -41,11 +43,40 @@ func Glob(globPattern string, options LoaderConfig) Loader {
// and any Loader options. Go-bindata usage.
// It returns a valid `Loader` which loads and maps the locale files.
//
// See `Glob`, `Assets`, `New` and `LoaderConfig` too.
// 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).
// With this method, all the embedded files into "sub" MUST be locale files.
//
// See `Glob`, `Assets`, `New` and `LoaderConfig` too.
func FS(fsOrDir interface{}, sub string, options LoaderConfig) (Loader, error) {
if sub == "" {
sub = "."
}
fileSystem := context.ResolveFS(fsOrDir)
assetNames, err := context.FindNames(fileSystem, sub)
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
}
// DefaultLoaderConfig represents the default loader configuration.
var DefaultLoaderConfig = LoaderConfig{
Left: "{{",