diff --git a/HISTORY.md b/HISTORY.md index d111a06e..1a00bdf1 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -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. diff --git a/_examples/README.md b/_examples/README.md index e4c9ec76..94a98c04 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -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)** diff --git a/_examples/i18n/template-embedded/locales/el-GR/other.ini b/_examples/i18n/template-embedded/locales/el-GR/other.ini new file mode 100644 index 00000000..5954cdd3 --- /dev/null +++ b/_examples/i18n/template-embedded/locales/el-GR/other.ini @@ -0,0 +1,10 @@ +[nav] +User = Λογαριασμός + +[debug] +Title = Μενού προγραμματιστή +AccessLog = Πρόσβαση στο αρχείο καταγραφής +AccessLogClear = Καθαρισμός {{tr "debug.AccessLog"}} + +[user.connections] +Title = {{tr "nav.User"}} Συνδέσεις \ No newline at end of file diff --git a/_examples/i18n/template-embedded/locales/el-GR/user.ini b/_examples/i18n/template-embedded/locales/el-GR/user.ini new file mode 100644 index 00000000..0a110cdc --- /dev/null +++ b/_examples/i18n/template-embedded/locales/el-GR/user.ini @@ -0,0 +1,4 @@ +[forms] +member = μέλος +register = Γίνε {{uppercase (tr "forms.member") }} +registered = εγγεγραμμένοι \ No newline at end of file diff --git a/_examples/i18n/template-embedded/locales/en-US/other.ini b/_examples/i18n/template-embedded/locales/en-US/other.ini new file mode 100644 index 00000000..31f1b182 --- /dev/null +++ b/_examples/i18n/template-embedded/locales/en-US/other.ini @@ -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 \ No newline at end of file diff --git a/_examples/i18n/template-embedded/locales/en-US/user.ini b/_examples/i18n/template-embedded/locales/en-US/user.ini new file mode 100644 index 00000000..09d3e3e4 --- /dev/null +++ b/_examples/i18n/template-embedded/locales/en-US/user.ini @@ -0,0 +1,4 @@ +[forms] +member = 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 new file mode 100644 index 00000000..fd45e57c --- /dev/null +++ b/_examples/i18n/template-embedded/main.go @@ -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 +} diff --git a/_examples/i18n/template-embedded/main_test.go b/_examples/i18n/template-embedded/main_test.go new file mode 100644 index 00000000..3aa2788b --- /dev/null +++ b/_examples/i18n/template-embedded/main_test.go @@ -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("Λογαριασμός Συνδέσεις") +} diff --git a/context/context_fs.go b/context/context_fs.go index 9495cf5d..7a8f423a 100644 --- a/context/context_fs.go +++ b/context/context_fs.go @@ -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 +} diff --git a/core/router/fs.go b/core/router/fs.go index 0864bedc..e6a91532 100644 --- a/core/router/fs.go +++ b/core/router/fs.go @@ -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 diff --git a/go.mod b/go.mod index 6c5ff7ec..117c0f4e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c9b235b1..f93b24be 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/i18n/i18n.go b/i18n/i18n.go index 2bcfbefd..29ca9fa5 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -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) diff --git a/i18n/loader.go b/i18n/loader.go index 511b57db..a38b7156 100644 --- a/i18n/loader.go +++ b/i18n/loader.go @@ -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: "{{",