New: i18n pluralization and variables support and more...

fixes: #1649, #1648, #1641, #1650

relative to: #1597
This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-09-29 19:19:19 +03:00
parent f224ded740
commit 4065819688
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
63 changed files with 2054 additions and 684 deletions

View File

@ -23,8 +23,310 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
# Next
This release introduces new features and some breaking changes inside the `mvc` and `hero` packages.
The codebase for dependency injection has been simplified a lot (fewer LOCs and easier to read and follow up).
This release introduces new features and some breaking changes.
The codebase for Dependency Injection, Internationalization and localization and more have been simplified a lot (fewer LOCs and easier to read and follow up).
## Fixes and Improvements
- Add builtin support for **[i18n pluralization](https://github.com/kataras/iris/tree/master/_examples/i18n/plurals)**. Please check out the [following yaml locale example](https://github.com/kataras/iris/tree/master/_examples/i18n/plurals/locales/en-US/welcome.yml) to see an overview of the supported formats.
- Fix [#1650](https://github.com/kataras/iris/issues/1650)
- Fix [#1649](https://github.com/kataras/iris/issues/1649)
- Fix [#1648](https://github.com/kataras/iris/issues/1648)
- Fix [#1641](https://github.com/kataras/iris/issues/1641)
- Add `Party.SetRoutesNoLog(disable bool) Party` to disable (the new) verbose logging of next routes.
- Add `mvc.Application.SetControllersNoLog(disable bool) *mvc.Application` to disable (the new) verbose logging of next controllers. As requested at [#1630](https://github.com/kataras/iris/issues/1630).
- Fix [#1621](https://github.com/kataras/iris/issues/1621) and add a new `cache.WithKey` to customize the cached entry key.
- Add a `Response() *http.Response` to the Response Recorder.
- Fix Response Recorder `Flush` when transfer-encoding is `chunked`.
- Fix Response Recorder `Clone` concurrent access afterwards.
- Add a `ParseTemplate` method on view engines to manually parse and add a template from a text as [requested](https://github.com/kataras/iris/issues/1617). [Examples](https://github.com/kataras/iris/tree/master/_examples/view/parse-template).
- Full `http.FileSystem` interface support for all **view** engines as [requested](https://github.com/kataras/iris/issues/1575). The first argument of the functions(`HTML`, `Blocks`, `Pug`, `Amber`, `Ace`, `Jet`, `Django`, `Handlebars`) can now be either a directory of `string` type (like before) or a value which completes the `http.FileSystem` interface. The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string.
- Add `Route.ExcludeSitemap() *Route` to exclude a route from sitemap as requested in [chat](https://chat.iris-go.com), also offline routes are excluded automatically now.
- Improved tracing (with `app.Logger().SetLevel("debug")`) for routes. Screens:
#### DBUG Routes (1)
![DBUG routes 1](https://iris-go.com/images/v12.2.0-dbug.png?v=0)
#### DBUG Routes (2)
![DBUG routes 2](https://iris-go.com/images/v12.2.0-dbug2.png?v=0)
#### DBUG Routes (3)
![DBUG routes with Controllers](https://iris-go.com/images/v12.2.0-dbug3.png?v=0)
- Update the [pprof middleware](https://github.com/kataras/iris/tree/master/middleware/pprof).
- New `Controller.HandleHTTPError(mvc.Code) <T>` optional Controller method to handle http errors as requested at: [MVC - More Elegent OnErrorCode registration?](https://github.com/kataras/iris/issues/1595). Example can be found [here](https://github.com/kataras/iris/tree/master/_examples/mvc/error-handler-http/main.go).
![MVC: HTTP Error Handler Method](https://user-images.githubusercontent.com/22900943/90948989-e04cd300-e44c-11ea-8c97-54d90fb0cbb6.png)
- New [Rewrite Engine Middleware](https://github.com/kataras/iris/tree/master/middleware/rewrite). Set up redirection rules for path patterns using the syntax we all know. [Example Code](https://github.com/kataras/iris/tree/master/_examples/routing/rewrite).
```yml
RedirectMatch: # REDIRECT_CODE_DIGITS | PATTERN_REGEX | TARGET_REPL
# Redirects /seo/* to /*
- 301 /seo/(.*) /$1
# Redirects /docs/v12* to /docs
- 301 /docs/v12(.*) /docs
# Redirects /old(.*) to /
- 301 /old(.*) /
# Redirects http or https://test.* to http or https://newtest.*
- 301 ^(http|https)://test.(.*) $1://newtest.$2
# Handles /*.json or .xml as *?format=json or xml,
# without redirect. See /users route.
# When Code is 0 then it does not redirect the request,
# instead it changes the request URL
# and leaves a route handle the request.
- 0 /(.*).(json|xml) /$1?format=$2
# Redirects root domain to www.
# Creation of a www subdomain inside the Application is unnecessary,
# all requests are handled by the root Application itself.
PrimarySubdomain: www
```
- New `TraceRoute bool` on [middleware/logger](https://github.com/kataras/iris/tree/master/middleware/logger) middleware. Displays information about the executed route. Also marks the handlers executed. Screenshot:
![logger middleware: TraceRoute screenshot](https://iris-go.com/images/github/logger-trace-route.png)
- Implement feature request [Log when I18n Translation Fails?](https://github.com/kataras/iris/issues/1593) by using the new `Application.I18n.DefaultMessageFunc` field **before** `I18n.Load`. [Example of usage](https://github.com/kataras/iris/blob/master/_examples/i18n/basic/main.go#L28-L50).
- Fix [#1594](https://github.com/kataras/iris/issues/1594) and add a new `PathAfterHandler` which can be set to true to enable the old behavior (not recommended though).
- New [apps](https://github.com/kataras/iris/tree/master/apps) subpackage. [Example of usage](https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect/multi-instances).
![apps image example](https://user-images.githubusercontent.com/22900943/90459288-8a54f400-e109-11ea-8dea-20631975c9fc.png)
- Fix `AutoTLS` when used with `iris.TLSNoRedirect` [*](https://github.com/kataras/iris/issues/1577). The `AutoTLS` runner can be customized through the new `iris.AutoTLSNoRedirect` instead, read its go documentation. Example of having both TLS and non-TLS versions of the same application without conflicts with letsencrypt `./well-known` path:
![](https://iris-go.com/images/github/autotls-1.png)
```go
package main
import (
"net/http"
"time"
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Logger().SetLevel("debug")
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{
"time": time.Now().Unix(),
"tls": ctx.Request().TLS != nil,
})
})
var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server {
srv := &http.Server{Handler: acme(app)}
go srv.ListenAndServe()
return srv
}
app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com",
iris.AutoTLSNoRedirect(fallbackServer)))
}
```
- `iris.Minify` middleware to minify responses based on their media/content-type.
- `Context.OnCloseErr` and `Context.OnConnectionCloseErr` - to call a function of `func() error` instead of an `iris.Handler` when request is closed or manually canceled.
- `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code).
- `Party.UseRouter(...Handler) and Party.ResetRouterFilters()` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS.
- `*versioning.Group` type is a full `Party` now.
- `Party.UseOnce` - either inserts a middleware, or on the basis of the middleware already existing, replace that existing middleware instead.
- Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully.
- Add [Blocks](_examples/view/template_blocks_0) template engine. <!-- Reminder for @kataras: follow https://github.com/flosch/pongo2/pull/236#issuecomment-668950566 discussion so we can get back on using the original pongo2 repository as they fixed the issue about an incompatible 3rd party package (although they need more fixes, that's why I commented there) -->
- Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements.
- Fix huge repo size of 55.7MB, which slows down the overall Iris installation experience. Now, go-get performs ~3 times faster. I 've managed it using the [bfg-repo-cleaner](https://github.com/rtyley/bfg-repo-cleaner) tool - an alternative to git-filter-branch command. Watch the small gif below to learn how:
[![](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif)](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif)
- [gRPC](https://grpc.io/) features:
- New Router [Wrapper](middleware/grpc).
- New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible).
- Add `Configuration.RemoteAddrHeadersForce bool` to force `Context.RemoteAddr() string` to return the first entry of request headers as a fallback instead of the `Request.RemoteAddr` one, as requested at: [1567#issuecomment-663972620](https://github.com/kataras/iris/issues/1567#issuecomment-663972620).
- Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177).
- Fix [#1564](https://github.com/kataras/iris/issues/1564).
- Fix [#1553](https://github.com/kataras/iris/issues/1553).
- New `DirOptions.Cache` to cache assets in-memory among with their compressed contents (in order to be ready to served if client ask). Learn more about this feature by reading [all #1556 comments](https://github.com/kataras/iris/issues/1556#issuecomment-661057446). Usage:
```go
var dirOpts = DirOptions{
// [...other options]
Cache: DirCacheOptions{
Enable: true,
// Don't compress files smaller than 300 bytes.
CompressMinSize: 300,
// Ignore compress already compressed file types
// (some images and pdf).
CompressIgnore: iris.MatchImagesAssets,
// Gzip, deflate, br(brotli), snappy.
Encodings: []string{"gzip", "deflate", "br", "snappy"},
// Log to the stdout the total reduced file size.
Verbose: 1,
},
}
```
- New `DirOptions.PushTargets` and `PushTargetsRegexp` to push index' assets to the client without additional requests. Inspirated by issue [#1562](https://github.com/kataras/iris/issues/1562). Example matching all `.js, .css and .ico` files (recursively):
```go
var dirOpts = iris.DirOptions{
// [...other options]
IndexName: "/index.html",
PushTargetsRegexp: map[string]*regexp.Regexp{
"/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"),
// OR:
// "/": iris.MatchCommonAssets,
},
Compress: true,
}
```
- Update jet parser to v5.0.2, closes [#1551](https://github.com/kataras/iris/issues/1551). It contains two breaking changes by its author:
- Relative paths on `extends, import, include...` tmpl functions, e.g. `{{extends "../layouts/application.jet"}}` instead of `layouts/application.jet`
- the new [jet.Ranger](https://github.com/CloudyKit/jet/pull/165) interface now requires a `ProvidesIndex() bool` method too
- Example has been [updated](https://github.com/kataras/iris/tree/master/_examples/view/template_jet_0)
- Fix [#1552](https://github.com/kataras/iris/issues/1552).
- Proper listing of root directories on `Party.HandleDir` when its `DirOptions.ShowList` was set to true.
- Customize the file/directory listing page through views, see [example](https://github.com/kataras/iris/tree/master/_examples/file-server/file-server).
- Socket Sharding as requested at [#1544](https://github.com/kataras/iris/issues/1544). New `iris.WithSocketSharding` Configurator and `SocketSharding bool` setting.
- Versioned Controllers feature through the new `mvc.Version` option. See [_examples/mvc/versioned-controller](https://github.com/kataras/iris/blob/master/_examples/mvc/versioned-controller/main.go).
- Fix [#1539](https://github.com/kataras/iris/issues/1539).
- New [rollbar example](https://github.com/kataras/iris/blob/master/_examples/logging/rollbar/main.go).
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary.
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
- `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (when `iris.WithEmptyFormError` is set) on missing form body to read from.
- `Configuration.EnablePathIntelligence | iris.WithPathIntelligence` to enable path intelligence automatic path redirection on the most closest path (if any), [example]((https://github.com/kataras/iris/blob/master/_examples/routing/intelligence/main.go)
- Enhanced cookie security and management through new `Context.AddCookieOptions` method and new cookie options (look on New Package-level functions section below), [securecookie](https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie) example has been updated.
- `Context.RemoveCookie` removes also the Request's specific cookie of the same request lifecycle when `iris.CookieAllowReclaim` is set to cookie options, [example](https://github.com/kataras/iris/tree/master/_examples/cookies/options).
- `iris.TLS` can now accept certificates in form of raw `[]byte` contents too.
- `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS`, e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. There is `iris.AutoTLSNoRedirect` option for `AutoTLS` too.
- Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code.
- Server will not return neither log the `ErrServerClosed` error if `app.Shutdown` was called manually via interrupt signal(CTRL/CMD+C), note that if the server closed by any other reason the error will be fired as previously (unless `iris.WithoutServerError(iris.ErrServerClosed)`).
- Finally, Log level's and Route debug information colorization is respected across outputs. Previously if the application used more than one output destination (e.g. a file through `app.Logger().AddOutput`) the color support was automatically disabled from all, including the terminal one, this problem is fixed now. Developers can now see colors in their terminals while log files are kept with clear text.
- New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching.
- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/request-body/read-json-struct-validation) example was updated.
- A result of <T> can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body.
- `ctx.JSON, JSONP, XML`: if `iris.WithOptimizations` is NOT passed on `app.Run/Listen` then the indentation defaults to `" "` (four spaces) and `" "` respectfully otherwise it is empty or the provided value.
- Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now.
- Improve Remote Address parsing as requested at: [#1453](https://github.com/kataras/iris/issues/1453). Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`.
- Fix [#1487](https://github.com/kataras/iris/issues/1487).
- Fix [#1473](https://github.com/kataras/iris/issues/1473).
## New Package-level Variables
- `iris.DirListRichOptions` to pass on `iris.DirListRich` method.
- `iris.DirListRich` to override the default look and feel if the `DirOptions.ShowList` was set to true, can be passed to `DirOptions.DirList` field.
- `DirOptions.PushTargets` for http/2 push on index [*](https://github.com/kataras/iris/tree/master/_examples/file-server/http2push/main.go).
- `iris.Compression` middleware to compress responses and decode compressed request data respectfully.
- `iris.B, KB, MB, GB, TB, PB, EB` for byte units.
- `TLSNoRedirect` to disable automatic "http://" to "https://" redirections (see below)
- `CookieAllowReclaim`, `CookieAllowSubdomains`, `CookieSameSite`, `CookieSecure` and `CookieEncoding` to bring previously sessions-only features to all cookies in the request.
## New Context Methods
- `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text).
- `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel.
- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced.
- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
- `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk.
- `Context.URLParamSlice(name string) []string` is a a shortcut of `ctx.Request().URL.Query()[name]`. Like `URLParam` but it returns all values as a string slice instead of a single string separated by commas.
- `Context.PostValueMany(name string) (string, error)` returns the post data of a given key. The returned value is a single string separated by commas on multiple values. It also reports whether the form was empty or when the "name" does not exist or whether the available values are empty. It strips any empty key-values from the slice before return. See `ErrEmptyForm`, `ErrNotFound` and `ErrEmptyFormField` respectfully. The `PostValueInt`, `PostValueInt64`, `PostValueFloat64` and `PostValueBool` now respect the above errors too (the `PostValues` method now returns a second output argument of `error` too, see breaking changes below).
- `Context.URLParamsSorted() []memstore.StringEntry` returns a sorted (by key) slice of key-value entries of the URL Query parameters.
- `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine).
- `Context.SetErr(error)` and `Context.GetErr() error` helpers.
- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`.
- `Context.Clone() Context` returns a copy of the Context safe for concurrent access.
- `Context.IsCanceled() bool` reports whether the request has been canceled by the client.
- `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too).
- `Context.CompressReader(enable bool)` method and `iris.CompressReader` middleware to enable future request read body calls to decompress data, [example](_examples/compression/main.go).
- `Context.RegisterDependency(v interface{})` and `Context.UnregisterDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware.
- `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary.
- `Context.Scheme() string` returns the full scheme of the request URL.
- `Context.SubdomainFull() string` returns the full subdomain(s) part of the host (`host[0:rootLevelDomain]`).
- `Context.Domain() string` returns the root level domain.
- `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request.
- `Context.ClearCookieOptions()` clears any cookie options registered through `AddCookieOptions`.
- `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc`
- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client
- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
- `Context.IsGRPC() bool` reports whether the request came from a gRPC client
- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
- `StopWithText(statusCode int, format string, args ...interface{})` stops the handlers chain, writes thre status code and a plain text message
- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
- `Context.Protobuf(proto.Message)` sends protobuf to the client (note that the `Context.JSON` is able to send protobuf as JSON)
- `Context.MsgPack(interface{})` sends msgpack format data to the client
- `Context.ReadProtobuf(ptr)` binds request body to a proto message
- `Context.ReadJSONProtobuf(ptr, ...options)` binds JSON request body to a proto message
- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
## MVC & Dependency Injection
The new release contains a fresh new and awesome feature....**a function dependency can accept previous registered dependencies and update or return a new value of any type**.
@ -362,302 +664,9 @@ Response:
}
```
Other Improvements:
- Add `Party.SetRoutesNoLog(disable bool) Party` to disable (the new) verbose logging of next routes.
- Add `mvc.Application.SetControllersNoLog(disable bool) *mvc.Application` to disable (the new) verbose logging of next controllers. As requested at [#1630](https://github.com/kataras/iris/issues/1630).
- Fix [#1621](https://github.com/kataras/iris/issues/1621) and add a new `cache.WithKey` to customize the cached entry key.
- Add a `Response() *http.Response` to the Response Recorder.
- Fix Response Recorder `Flush` when transfer-encoding is `chunked`.
- Fix Response Recorder `Clone` concurrent access afterwards.
- Add a `ParseTemplate` method on view engines to manually parse and add a template from a text as [requested](https://github.com/kataras/iris/issues/1617). [Examples](https://github.com/kataras/iris/tree/master/_examples/view/parse-template).
- Full `http.FileSystem` interface support for all **view** engines as [requested](https://github.com/kataras/iris/issues/1575). The first argument of the functions(`HTML`, `Blocks`, `Pug`, `Amber`, `Ace`, `Jet`, `Django`, `Handlebars`) can now be either a directory of `string` type (like before) or a value which completes the `http.FileSystem` interface. The `.Binary` method of all view engines was removed: pass the go-bindata's latest version `AssetFile()` exported function as the first argument instead of string.
- Add `Route.ExcludeSitemap() *Route` to exclude a route from sitemap as requested in [chat](https://chat.iris-go.com), also offline routes are excluded automatically now.
- Improved tracing (with `app.Logger().SetLevel("debug")`) for routes. Screens:
#### DBUG Routes (1)
![DBUG routes 1](https://iris-go.com/images/v12.2.0-dbug.png?v=0)
#### DBUG Routes (2)
![DBUG routes 2](https://iris-go.com/images/v12.2.0-dbug2.png?v=0)
#### DBUG Routes (3)
![DBUG routes with Controllers](https://iris-go.com/images/v12.2.0-dbug3.png?v=0)
- Update the [pprof middleware](https://github.com/kataras/iris/tree/master/middleware/pprof).
- New `Controller.HandleHTTPError(mvc.Code) <T>` optional Controller method to handle http errors as requested at: [MVC - More Elegent OnErrorCode registration?](https://github.com/kataras/iris/issues/1595). Example can be found [here](https://github.com/kataras/iris/tree/master/_examples/mvc/error-handler-http/main.go).
![MVC: HTTP Error Handler Method](https://user-images.githubusercontent.com/22900943/90948989-e04cd300-e44c-11ea-8c97-54d90fb0cbb6.png)
- New [Rewrite Engine Middleware](https://github.com/kataras/iris/tree/master/middleware/rewrite). Set up redirection rules for path patterns using the syntax we all know. [Example Code](https://github.com/kataras/iris/tree/master/_examples/routing/rewrite).
```yml
RedirectMatch: # REDIRECT_CODE_DIGITS | PATTERN_REGEX | TARGET_REPL
# Redirects /seo/* to /*
- 301 /seo/(.*) /$1
# Redirects /docs/v12* to /docs
- 301 /docs/v12(.*) /docs
# Redirects /old(.*) to /
- 301 /old(.*) /
# Redirects http or https://test.* to http or https://newtest.*
- 301 ^(http|https)://test.(.*) $1://newtest.$2
# Handles /*.json or .xml as *?format=json or xml,
# without redirect. See /users route.
# When Code is 0 then it does not redirect the request,
# instead it changes the request URL
# and leaves a route handle the request.
- 0 /(.*).(json|xml) /$1?format=$2
# Redirects root domain to www.
# Creation of a www subdomain inside the Application is unnecessary,
# all requests are handled by the root Application itself.
PrimarySubdomain: www
```
- New `TraceRoute bool` on [middleware/logger](https://github.com/kataras/iris/tree/master/middleware/logger) middleware. Displays information about the executed route. Also marks the handlers executed. Screenshot:
![logger middleware: TraceRoute screenshot](https://iris-go.com/images/github/logger-trace-route.png)
- Implement feature request [Log when I18n Translation Fails?](https://github.com/kataras/iris/issues/1593) by using the new `Application.I18n.DefaultMessageFunc` field **before** `I18n.Load`. [Example of usage](https://github.com/kataras/iris/blob/master/_examples/i18n/main.go#L28-L50).
- Fix [#1594](https://github.com/kataras/iris/issues/1594) and add a new `PathAfterHandler` which can be set to true to enable the old behavior (not recommended though).
- New [apps](https://github.com/kataras/iris/tree/master/apps) subpackage. [Example of usage](https://github.com/kataras/iris/tree/master/_examples/routing/subdomains/redirect/multi-instances).
![apps image example](https://user-images.githubusercontent.com/22900943/90459288-8a54f400-e109-11ea-8dea-20631975c9fc.png)
- Fix `AutoTLS` when used with `iris.TLSNoRedirect` [*](https://github.com/kataras/iris/issues/1577). The `AutoTLS` runner can be customized through the new `iris.AutoTLSNoRedirect` instead, read its go documentation. Example of having both TLS and non-TLS versions of the same application without conflicts with letsencrypt `./well-known` path:
![](https://iris-go.com/images/github/autotls-1.png)
```go
package main
import (
"net/http"
"time"
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Logger().SetLevel("debug")
app.Get("/", func(ctx iris.Context) {
ctx.JSON(iris.Map{
"time": time.Now().Unix(),
"tls": ctx.Request().TLS != nil,
})
})
var fallbackServer = func(acme func(http.Handler) http.Handler) *http.Server {
srv := &http.Server{Handler: acme(app)}
go srv.ListenAndServe()
return srv
}
app.Run(iris.AutoTLS(":443", "example.com", "mail@example.com",
iris.AutoTLSNoRedirect(fallbackServer)))
}
```
- `iris.Minify` middleware to minify responses based on their media/content-type.
- `Context.OnCloseErr` and `Context.OnConnectionCloseErr` - to call a function of `func() error` instead of an `iris.Handler` when request is closed or manually canceled.
- `Party.UseError(...Handler)` - to register handlers to run before any http errors (e.g. before `OnErrorCode/OnAnyErrorCode` or default error codes when no handler is responsible to handle a specific http status code).
- `Party.UseRouter(...Handler) and Party.ResetRouterFilters()` - to register handlers before the main router, useful on handlers that should control whether the router itself should ran or not. Independently of the incoming request's method and path values. These handlers will be executed ALWAYS against ALL incoming matched requests. Example of use-case: CORS.
- `*versioning.Group` type is a full `Party` now.
- `Party.UseOnce` - either inserts a middleware, or on the basis of the middleware already existing, replace that existing middleware instead.
- Ability to register a view engine per group of routes or for the current chain of handlers through `Party.RegisterView` and `Context.ViewEngine` respectfully.
- Add [Blocks](_examples/view/template_blocks_0) template engine. <!-- Reminder for @kataras: follow https://github.com/flosch/pongo2/pull/236#issuecomment-668950566 discussion so we can get back on using the original pongo2 repository as they fixed the issue about an incompatible 3rd party package (although they need more fixes, that's why I commented there) -->
- Add [Ace](_examples/view/template_ace_0) template parser to the view engine and other minor improvements.
- Fix huge repo size of 55.7MB, which slows down the overall Iris installation experience. Now, go-get performs ~3 times faster. I 've managed it using the [bfg-repo-cleaner](https://github.com/rtyley/bfg-repo-cleaner) tool - an alternative to git-filter-branch command. Watch the small gif below to learn how:
[![](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif)](https://media.giphy.com/media/U8560aiWTurW4iAOLn/giphy.gif)
- [gRPC](https://grpc.io/) features:
- New Router [Wrapper](middleware/grpc).
- New MVC `.Handle(ctrl, mvc.GRPC{...})` option which allows to register gRPC services per-party (without the requirement of a full wrapper) and optionally strict access to gRPC clients only, see the [example here](_examples/mvc/grpc-compatible).
- Add `Configuration.RemoteAddrHeadersForce bool` to force `Context.RemoteAddr() string` to return the first entry of request headers as a fallback instead of the `Request.RemoteAddr` one, as requested at: [1567#issuecomment-663972620](https://github.com/kataras/iris/issues/1567#issuecomment-663972620).
- Fix [#1569#issuecomment-663739177](https://github.com/kataras/iris/issues/1569#issuecomment-663739177).
- Fix [#1564](https://github.com/kataras/iris/issues/1564).
- Fix [#1553](https://github.com/kataras/iris/issues/1553).
- New `DirOptions.Cache` to cache assets in-memory among with their compressed contents (in order to be ready to served if client ask). Learn more about this feature by reading [all #1556 comments](https://github.com/kataras/iris/issues/1556#issuecomment-661057446). Usage:
```go
var dirOpts = DirOptions{
// [...other options]
Cache: DirCacheOptions{
Enable: true,
// Don't compress files smaller than 300 bytes.
CompressMinSize: 300,
// Ignore compress already compressed file types
// (some images and pdf).
CompressIgnore: iris.MatchImagesAssets,
// Gzip, deflate, br(brotli), snappy.
Encodings: []string{"gzip", "deflate", "br", "snappy"},
// Log to the stdout the total reduced file size.
Verbose: 1,
},
}
```
- New `DirOptions.PushTargets` and `PushTargetsRegexp` to push index' assets to the client without additional requests. Inspirated by issue [#1562](https://github.com/kataras/iris/issues/1562). Example matching all `.js, .css and .ico` files (recursively):
```go
var dirOpts = iris.DirOptions{
// [...other options]
IndexName: "/index.html",
PushTargetsRegexp: map[string]*regexp.Regexp{
"/": regexp.MustCompile("((.*).js|(.*).css|(.*).ico)$"),
// OR:
// "/": iris.MatchCommonAssets,
},
Compress: true,
}
```
- Update jet parser to v5.0.2, closes [#1551](https://github.com/kataras/iris/issues/1551). It contains two breaking changes by its author:
- Relative paths on `extends, import, include...` tmpl functions, e.g. `{{extends "../layouts/application.jet"}}` instead of `layouts/application.jet`
- the new [jet.Ranger](https://github.com/CloudyKit/jet/pull/165) interface now requires a `ProvidesIndex() bool` method too
- Example has been [updated](https://github.com/kataras/iris/tree/master/_examples/view/template_jet_0)
- Fix [#1552](https://github.com/kataras/iris/issues/1552).
- Proper listing of root directories on `Party.HandleDir` when its `DirOptions.ShowList` was set to true.
- Customize the file/directory listing page through views, see [example](https://github.com/kataras/iris/tree/master/_examples/file-server/file-server).
- Socket Sharding as requested at [#1544](https://github.com/kataras/iris/issues/1544). New `iris.WithSocketSharding` Configurator and `SocketSharding bool` setting.
- Versioned Controllers feature through the new `mvc.Version` option. See [_examples/mvc/versioned-controller](https://github.com/kataras/iris/blob/master/_examples/mvc/versioned-controller/main.go).
- Fix [#1539](https://github.com/kataras/iris/issues/1539).
- New [rollbar example](https://github.com/kataras/iris/blob/master/_examples/logging/rollbar/main.go).
- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware.
- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary.
- New `iris.RouteOverlap` route registration rule. `Party.SetRegisterRule(iris.RouteOverlap)` to allow overlapping across multiple routes for the same request subdomain, method, path. See [1536#issuecomment-643719922](https://github.com/kataras/iris/issues/1536#issuecomment-643719922). This allows two or more **MVC Controllers** to listen on the same path based on one or more registered dependencies (see [_examples/mvc/authenticated-controller](https://github.com/kataras/iris/tree/master/_examples/mvc/authenticated-controller)).
- `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (when `iris.WithEmptyFormError` is set) on missing form body to read from.
- `Configuration.EnablePathIntelligence | iris.WithPathIntelligence` to enable path intelligence automatic path redirection on the most closest path (if any), [example]((https://github.com/kataras/iris/blob/master/_examples/routing/intelligence/main.go)
- Enhanced cookie security and management through new `Context.AddCookieOptions` method and new cookie options (look on New Package-level functions section below), [securecookie](https://github.com/kataras/iris/tree/master/_examples/cookies/securecookie) example has been updated.
- `Context.RemoveCookie` removes also the Request's specific cookie of the same request lifecycle when `iris.CookieAllowReclaim` is set to cookie options, [example](https://github.com/kataras/iris/tree/master/_examples/cookies/options).
- `iris.TLS` can now accept certificates in form of raw `[]byte` contents too.
- `iris.TLS` registers a secondary http server which redirects "http://" to their "https://" equivalent requests, unless the new `iris.TLSNoRedirect` host Configurator is provided on `iris.TLS`, e.g. `app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key", iris.TLSNoRedirect))`. There is `iris.AutoTLSNoRedirect` option for `AutoTLS` too.
- Fix an [issue](https://github.com/kataras/i18n/issues/1) about i18n loading from path which contains potential language code.
- Server will not return neither log the `ErrServerClosed` error if `app.Shutdown` was called manually via interrupt signal(CTRL/CMD+C), note that if the server closed by any other reason the error will be fired as previously (unless `iris.WithoutServerError(iris.ErrServerClosed)`).
- Finally, Log level's and Route debug information colorization is respected across outputs. Previously if the application used more than one output destination (e.g. a file through `app.Logger().AddOutput`) the color support was automatically disabled from all, including the terminal one, this problem is fixed now. Developers can now see colors in their terminals while log files are kept with clear text.
- New `iris.WithLowercaseRouting` option which forces all routes' paths to be lowercase and converts request paths to their lowercase for matching.
- New `app.Validator { Struct(interface{}) error }` field and `app.Validate` method were added. The `app.Validator = ` can be used to integrate a 3rd-party package such as [go-playground/validator](https://github.com/go-playground/validator). If set-ed then Iris `Context`'s `ReadJSON`, `ReadXML`, `ReadMsgPack`, `ReadYAML`, `ReadForm`, `ReadQuery`, `ReadBody` methods will return the validation error on data validation failures. The [read-json-struct-validation](_examples/request-body/read-json-struct-validation) example was updated.
- A result of <T> can implement the new `hero.PreflightResult` interface which contains a single method of `Preflight(iris.Context) error`. If this method exists on a custom struct value which is returned from a handler then it will fire that `Preflight` first and if not errored then it will cotninue by sending the struct value as JSON(by-default) response body.
- `ctx.JSON, JSONP, XML`: if `iris.WithOptimizations` is NOT passed on `app.Run/Listen` then the indentation defaults to `" "` (four spaces) and `" "` respectfully otherwise it is empty or the provided value.
- Hero Handlers (and `app.ConfigureContainer().Handle`) do not have to require `iris.Context` just to call `ctx.Next()` anymore, this is done automatically now.
- Improve Remote Address parsing as requested at: [#1453](https://github.com/kataras/iris/issues/1453). Add `Configuration.RemoteAddrPrivateSubnets` to exclude those addresses when fetched by `Configuration.RemoteAddrHeaders` through `context.RemoteAddr() string`.
- Fix [#1487](https://github.com/kataras/iris/issues/1487).
- Fix [#1473](https://github.com/kataras/iris/issues/1473).
New Package-level Variables:
- `iris.DirListRichOptions` to pass on `iris.DirListRich` method.
- `iris.DirListRich` to override the default look and feel if the `DirOptions.ShowList` was set to true, can be passed to `DirOptions.DirList` field.
- `DirOptions.PushTargets` for http/2 push on index [*](https://github.com/kataras/iris/tree/master/_examples/file-server/http2push/main.go).
- `iris.Compression` middleware to compress responses and decode compressed request data respectfully.
- `iris.B, KB, MB, GB, TB, PB, EB` for byte units.
- `TLSNoRedirect` to disable automatic "http://" to "https://" redirections (see below)
- `CookieAllowReclaim`, `CookieAllowSubdomains`, `CookieSameSite`, `CookieSecure` and `CookieEncoding` to bring previously sessions-only features to all cookies in the request.
New Context Methods:
- `Context.TextYAML(interface{}) error` same as `Context.YAML` but with set the Content-Type to `text/yaml` instead (Google Chrome renders it as text).
- `Context.IsDebug() bool` reports whether the application is running under debug/development mode. It is a shortcut of Application.Logger().Level >= golog.DebugLevel.
- `Context.IsRecovered() bool` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced.
- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
- `Context.ReadParams(ptr interface{}) error` binds dynamic path parameters to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-params/main.go).
- `Context.SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error)` previously unexported. Accepts a result file of `Context.FormFile` and saves it to the disk.
- `Context.URLParamSlice(name string) []string` is a a shortcut of `ctx.Request().URL.Query()[name]`. Like `URLParam` but it returns all values as a string slice instead of a single string separated by commas.
- `Context.PostValueMany(name string) (string, error)` returns the post data of a given key. The returned value is a single string separated by commas on multiple values. It also reports whether the form was empty or when the "name" does not exist or whether the available values are empty. It strips any empty key-values from the slice before return. See `ErrEmptyForm`, `ErrNotFound` and `ErrEmptyFormField` respectfully. The `PostValueInt`, `PostValueInt64`, `PostValueFloat64` and `PostValueBool` now respect the above errors too (the `PostValues` method now returns a second output argument of `error` too, see breaking changes below).
- `Context.URLParamsSorted() []memstore.StringEntry` returns a sorted (by key) slice of key-value entries of the URL Query parameters.
- `Context.ViewEngine(ViewEngine)` to set a view engine on-fly for the current chain of handlers, responsible to render templates through `ctx.View`. [Example](_examples/view/context-view-engine).
- `Context.SetErr(error)` and `Context.GetErr() error` helpers.
- `Context.CompressWriter(bool) error` and `Context.CompressReader(bool) error`.
- `Context.Clone() Context` returns a copy of the Context safe for concurrent access.
- `Context.IsCanceled() bool` reports whether the request has been canceled by the client.
- `Context.IsSSL() bool` reports whether the request is under HTTPS SSL (New `Configuration.SSLProxyHeaders` and `HostProxyHeaders` fields too).
- `Context.CompressReader(enable bool)` method and `iris.CompressReader` middleware to enable future request read body calls to decompress data, [example](_examples/compression/main.go).
- `Context.RegisterDependency(v interface{})` and `Context.UnregisterDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware.
- `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary.
- `Context.Scheme() string` returns the full scheme of the request URL.
- `Context.SubdomainFull() string` returns the full subdomain(s) part of the host (`host[0:rootLevelDomain]`).
- `Context.Domain() string` returns the root level domain.
- `Context.AddCookieOptions(...CookieOption)` adds options for `SetCookie`, `SetCookieKV, UpsertCookie` and `RemoveCookie` methods for the current request.
- `Context.ClearCookieOptions()` clears any cookie options registered through `AddCookieOptions`.
- `Context.SetLanguage(langCode string)` force-sets a language code from inside a middleare, similar to the `app.I18n.ExtractFunc`
- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client
- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2
- `Context.IsGRPC() bool` reports whether the request came from a gRPC client
- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too
- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code
- `StopWithText(statusCode int, format string, args ...interface{})` stops the handlers chain, writes thre status code and a plain text message
- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message
- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response
- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response
- `Context.Protobuf(proto.Message)` sends protobuf to the client (note that the `Context.JSON` is able to send protobuf as JSON)
- `Context.MsgPack(interface{})` sends msgpack format data to the client
- `Context.ReadProtobuf(ptr)` binds request body to a proto message
- `Context.ReadJSONProtobuf(ptr, ...options)` binds JSON request body to a proto message
- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct
- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type
- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)`
- `Context.Controller() reflect.Value` returns the current MVC Controller value.
Breaking Changes:
## Breaking Changes
- Now that `RegisterView` can be used to register different view engines per-Party, there is no need to support registering multiple engines under the same Party. The `app.RegisterView` now upserts the given Engine instead of append. You can now render templates **without file extension**, e.g. `index` instead of `index.ace`, both forms are valid now.
- The `Context.ContentType` does not accept filenames to resolve the mime type anymore (caused issues with vendor-specific(vnd) MIME types).
- The `Configuration.RemoteAddrPrivateSubnets.IPRange.Start and End` are now type of `string` instead of `net.IP`. The `WithRemoteAddrPrivateSubnet` option remains as it is, already accepts `string`s.
- The `i18n#LoaderConfig.FuncMap template.FuncMap` field was replaced with `Funcs func(iris.Locale) template.FuncMap` in order to give current locale access to the template functions. A new `app.I18n.Loader` was introduced too, in order to make it easier for end-developers to customize the translation key values.
@ -691,8 +700,6 @@ Breaking Changes:
- `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead
- **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead.
There is a change on the type alias of `iris.Context` which now points to the `*context.Context` instead of the `context.Context` interface. The **interface has been removed** and the ability to **override** the Context **is not** available any more. When we added the ability from end-developers to override the Context years ago, we have never imagine that we will ever had such a featured Context with more than 4000 lines of code. As of Iris 2020, it is difficult and un-productive from an end-developer to override the Iris Context, and as far as we know, nobody uses this feature anymore because of that exact reason. Beside the overriding feature support end, if you still use the `context.Context` instead of `iris.Context`, it's the time to do it: please find-and-replace any `context.Context` to `iris.Context` as wikis, book and all examples shows for the past 3 years. For the 99.9% of the users there is no a single breaking change, you already using `iris.Context` so you are in the "safe zone".
# Su, 16 February 2020 | v12.1.8
New Features:
@ -793,7 +800,7 @@ All known issues.
### Internationalization and localization
Support for i18n is now a **builtin feature** and is being respected across your entire application, per say [sitemap](https://github.com/kataras/iris/wiki/Sitemap) and [views](https://github.com/kataras/iris/blob/master/_examples/i18n/main.go#L50).
Support for i18n is now a **builtin feature** and is being respected across your entire application, per say [sitemap](https://github.com/kataras/iris/wiki/Sitemap) and [views](https://github.com/kataras/iris/blob/master/_examples/i18n/basic/main.go#L50).
Refer to the wiki section: https://github.com/kataras/iris/wiki/Sitemap for details.

View File

@ -176,8 +176,9 @@
* [Client-Side](compression/client/main.go)
* [Client-Side (using Iris)](compress/client-using-iris/main.go)
* Localization and Internationalization
* [i18n](i18n)
* [i18n templates and functions](i18n/i18n-template)
* [Basic](i18n/basic)
* [Ttemplates and Functions](i18n/template)
* [Pluralization and Variables](i18n/plurals)
* Authentication, Authorization & Bot Detection
* [Basic Authentication](auth/basicauth/main.go)
* [CORS](auth/cors)

View File

@ -122,7 +122,7 @@ func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
refreshClaims := refreshJWT.Expiry(jwt.Claims{
ID: "refresh_kataras",
// For example, the User ID,
// this is nessecary to check against the database
// this is necessary to check against the database
// if the user still exist or has credentials to access our page.
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
})

View File

@ -0,0 +1 @@
key1: "αυτό είναι μια τιμή από το πρώτο αρχείο: locale_multi_first"

View File

@ -0,0 +1 @@
key1: "this is a value from the first file: locale_multi_first"

View File

@ -1,2 +0,0 @@
Dog: "σκυλί"
HiDogs: Γειά {{plural (tr "Dog") .count }}

View File

@ -1,2 +0,0 @@
Dog: "dog"
HiDogs: Hi {{plural (tr "Dog") .count }}

View File

@ -1 +0,0 @@
key1 = αυτό είναι μια τιμή από το πρώτο αρχείο: locale_multi_first

View File

@ -1 +0,0 @@
key1 = this is a value from the first file: locale_multi_first

View File

@ -0,0 +1,5 @@
[message]
Encrypted = Encrypted
Message = Message
EncryptedMessage = {{tr "message.Encrypted"}} {{tr "message.Message"}}
HostResult = Store {{tr "message.EncryptedMessage"}} Online

View File

@ -0,0 +1,89 @@
# Locale variables
#
# Unlike normal keys, the variables
# have limitations of: no ">x", "zero", "two" and template functions are supported.
# This is done to force developers to use small and easy to read variables for easier maintain process.
Vars:
- Minutes:
# possible keys:
# one
# "=x" - where x is a number
# "<x"
# other
# format - to customize the format, which defaults to %d .
one: "minute"
other: "minutes"
format: "%d" # defaults to that.
- Dogs:
"=5": "dogsssss"
one: "dog"
other: "dogs"
- Houses:
one: "house"
other: "houses"
- Gender:
"=1": "She" # 1 for female
"=2": "He" # 2 for male
# Using variables in raw string
YouLate: "You are %[1]d ${Minutes} late."
# Just a simple raw value
Classic: "classic"
# Pluralization, translate based on the plural count
# including the variables and their counts
FreeDay:
# possible keys:
# zero
# one
# two
# "=x"
# "<x"
# ">x"
# other
"=3": "You have three days and %[2]d ${Minutes} off." # "FreeDay" 3, 15 (plurals + variable pluralization)
one: "You have a day off" # "FreeDay", 1
other: "You have %[1]d free days" # "FreeDay", 5
# Sprintf-like raw translation
HeIsHome: "%s is home"
# Value without plural of its self but variables except pluralization
HouseCount: "${Gender} (%[3]s) has %[2]d ${Houses}"
# Same as above but with a template instead
VarTemplate: (${Gender}) {{tr "HeIsHome" .Name}}
# Template and non template with variables in the same plural key
VarTemplatePlural:
one: "${Gender} is awesome"
other: "other (${Gender}) has %[3]d ${Houses}"
"=5": "{{call .InlineJoin .Names}} are awesome"
TemplatePlural:
one: "{{.Name}} is unique"
"=5": "{{call .InlineJoin .Names}} are awesome"
# Same as above but it takes the variable counting through the map argument
TemplateVarTemplatePlural:
other: "These {{.PluralCount}} are wonderful, feeding {{.DogsCount}} ${Dogs} in total!"
# Local variables and section.
LocalVarsHouseCount:
Text: "${Gender} has %[2]d ${Houses}"
Vars:
- Gender:
"=3": "She"
"=4": "He"
- Houses:
one: "house"
other: "houses"
# Sections:
root:
user: Account
nav:
home: Home # nav.home
user: '{{tr "root.user"}}' # nav.user
more:
what: "this" # nav.more.what
even:
more: "yes" # nav.more.even.more
aplural: "You are %[1]d ${Minutes} late." # Tr("nav.more.even.aplural", 15)

View File

@ -0,0 +1,175 @@
package main
import (
"strings"
"github.com/kataras/iris/v12"
)
const (
female = iota + 1
male
)
const tableStyle = `
<style>
a {
padding: 8px 8px;
text-decoration:none;
cursor:pointer;
color: #10a2ff;
}
table {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
empty-cells: show;
border: 1px solid #cbcbcb;
}
table caption {
color: #000;
font: italic 85%/1 arial, sans-serif;
padding: 1em 0;
text-align: center;
}
table td,
table th {
border-left: 1px solid #cbcbcb;
border-width: 0 0 0 1px;
font-size: inherit;
margin: 0;
overflow: visible;
padding: 0.5em 1em;
}
table thead {
background-color: #10a2ff;
color: #fff;
text-align: left;
vertical-align: bottom;
}
table td {
background-color: transparent;
}
.table-odd td {
background-color: #f2f2f2;
}
.table-bordered td {
border-bottom: 1px solid #cbcbcb;
}
.table-bordered tbody > tr:last-child > td {
border-bottom-width: 0;
}
</style>
`
/*
$ go run .
Visit http://localhost:8080
*/
func main() {
app := iris.New()
err := app.I18n.Load("./locales/*/*", "en-US")
// ^ here we only use a single locale for the sake of the example,
// on a real app you can register as many languages as you want to support.
if err != nil {
panic(err)
}
app.Get("/", func(ctx iris.Context) {
ctx.HTML("<html><body>\n")
ctx.WriteString(tableStyle)
ctx.WriteString(`<table class="table-bordered table-odd">
<thead>
<tr>
<th>Key</th>
<th>Translation</th>
<th>Arguments</th>
</tr>
</thead><tbody>
`)
defer ctx.WriteString("</tbody></table></body></html>")
tr(ctx, "Classic")
tr(ctx, "YouLate", 1)
tr(ctx, "YouLate", 2)
tr(ctx, "FreeDay", 1)
tr(ctx, "FreeDay", 5)
tr(ctx, "FreeDay", 3, 15)
tr(ctx, "HeIsHome", "Peter")
tr(ctx, "HouseCount", female, 2, "Maria")
tr(ctx, "HouseCount", male, 1, "Peter")
tr(ctx, "nav.home")
tr(ctx, "nav.user")
tr(ctx, "nav.more.what")
tr(ctx, "nav.more.even.more")
tr(ctx, "nav.more.even.aplural", 1)
tr(ctx, "nav.more.even.aplural", 15)
tr(ctx, "VarTemplate", iris.Map{
"Name": "Peter",
"GenderCount": male,
})
tr(ctx, "VarTemplatePlural", 1, female)
tr(ctx, "VarTemplatePlural", 2, female, 1)
tr(ctx, "VarTemplatePlural", 2, female, 5)
tr(ctx, "VarTemplatePlural", 1, male)
tr(ctx, "VarTemplatePlural", 2, male, 1)
tr(ctx, "VarTemplatePlural", 2, male, 2)
tr(ctx, "VarTemplatePlural", iris.Map{
"PluralCount": 5,
"Names": []string{"Makis", "Peter"},
"InlineJoin": func(arr []string) string {
return strings.Join(arr, ", ")
},
})
tr(ctx, "TemplatePlural", iris.Map{
"PluralCount": 1,
"Name": "Peter",
})
tr(ctx, "TemplatePlural", iris.Map{
"PluralCount": 5,
"Names": []string{"Makis", "Peter"},
"InlineJoin": func(arr []string) string {
return strings.Join(arr, ", ")
},
})
tr(ctx, "VarTemplatePlural", 2, male, 4)
tr(ctx, "TemplateVarTemplatePlural", iris.Map{
"PluralCount": 3,
"DogsCount": 5,
})
tr(ctx, "message.HostResult")
tr(ctx, "LocalVarsHouseCount.Text", 3, 4)
})
app.Listen(":8080")
}
func tr(ctx iris.Context, key string, args ...interface{}) {
translation := ctx.Tr(key, args...)
ctx.Writef("<tr><td>%s</td><td>%s</td><td>%v</td></tr>\n", key, translation, args)
}

View File

@ -0,0 +1,131 @@
package main_test
import (
"strings"
"testing"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/httptest"
)
const (
female = iota + 1
male
)
func TestI18nPlurals(t *testing.T) {
handler := func(ctx iris.Context) {
tr(ctx, "Classic")
tr(ctx, "YouLate", 1)
tr(ctx, "YouLate", 2)
tr(ctx, "FreeDay", 1)
tr(ctx, "FreeDay", 5)
tr(ctx, "FreeDay", 3, 15)
tr(ctx, "HeIsHome", "Peter")
tr(ctx, "HouseCount", female, 2, "Maria")
tr(ctx, "HouseCount", male, 1, "Peter")
tr(ctx, "nav.home")
tr(ctx, "nav.user")
tr(ctx, "nav.more.what")
tr(ctx, "nav.more.even.more")
tr(ctx, "nav.more.even.aplural", 1)
tr(ctx, "nav.more.even.aplural", 15)
tr(ctx, "VarTemplate", iris.Map{
"Name": "Peter",
"GenderCount": male,
})
tr(ctx, "VarTemplatePlural", 1, female)
tr(ctx, "VarTemplatePlural", 2, female, 1)
tr(ctx, "VarTemplatePlural", 2, female, 5)
tr(ctx, "VarTemplatePlural", 1, male)
tr(ctx, "VarTemplatePlural", 2, male, 1)
tr(ctx, "VarTemplatePlural", 2, male, 2)
tr(ctx, "VarTemplatePlural", iris.Map{
"PluralCount": 5,
"Names": []string{"Makis", "Peter"},
"InlineJoin": func(arr []string) string {
return strings.Join(arr, ", ")
},
})
tr(ctx, "TemplatePlural", iris.Map{
"PluralCount": 1,
"Name": "Peter",
})
tr(ctx, "TemplatePlural", iris.Map{
"PluralCount": 5,
"Names": []string{"Makis", "Peter"},
"InlineJoin": func(arr []string) string {
return strings.Join(arr, ", ")
},
})
tr(ctx, "VarTemplatePlural", 2, male, 4)
tr(ctx, "TemplateVarTemplatePlural", iris.Map{
"PluralCount": 3,
"DogsCount": 5,
})
tr(ctx, "message.HostResult")
tr(ctx, "LocalVarsHouseCount.Text", 3, 4)
}
w := httptest.NewRecorder()
r := httptest.NewRequest("GET", "/", nil)
defer r.Body.Close()
httptest.Do(w, r, handler, func(app *iris.Application) {
err := app.I18n.Load("./locales/*/*", "en-US", "el-GR")
if err != nil {
panic(err)
}
})
expected := `Classic=classic
YouLate=You are 1 minute late.
YouLate=You are 2 minutes late.
FreeDay=You have a day off
FreeDay=You have 5 free days
FreeDay=You have three days and 15 minutes off.
HeIsHome=Peter is home
HouseCount=She (Maria) has 2 houses
HouseCount=He (Peter) has 1 house
nav.home=Home
nav.user=Account
nav.more.what=this
nav.more.even.more=yes
nav.more.even.aplural=You are 1 minute late.
nav.more.even.aplural=You are 15 minutes late.
VarTemplate=(He) Peter is home
VarTemplatePlural=She is awesome
VarTemplatePlural=other (She) has 1 house
VarTemplatePlural=other (She) has 5 houses
VarTemplatePlural=He is awesome
VarTemplatePlural=other (He) has 1 house
VarTemplatePlural=other (He) has 2 houses
VarTemplatePlural=Makis, Peter are awesome
TemplatePlural=Peter is unique
TemplatePlural=Makis, Peter are awesome
VarTemplatePlural=other (He) has 4 houses
TemplateVarTemplatePlural=These 3 are wonderful, feeding 5 dogsssss in total!
message.HostResult=Store Encrypted Message Online
LocalVarsHouseCount.Text=She has 4 houses
`
if got := w.Body.String(); expected != got {
t.Fatalf("expected:\n'%s'\n\nbut got:\n'%s'", expected, got)
}
}
func tr(ctx iris.Context, key string, args ...interface{}) {
translation := ctx.Tr(key, args...)
ctx.Writef("%s=%s\n", key, translation)
}

View File

@ -0,0 +1 @@
HiDogs: Hi %d {{plural (tr "Dog") .count }}

View File

@ -0,0 +1,2 @@
Dog: "dog"
HiDogs: Hi %d {{plural (tr "Dog") .count }}

View File

@ -5,13 +5,20 @@ import (
"text/template"
"github.com/kataras/iris/v12"
// go get -u github.com/gertd/go-pluralize
"github.com/gertd/go-pluralize"
// go get -u golang.org/x/text/message
"golang.org/x/text/feature/plural"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
/*
Iris I18n supports text/template inside the translation values.
Follow this example to learn how to use that feature.
This is just an example on how to use template functions.
See the "plurals" example for a more comprehensive pluralization support instead.
*/
func main() {
@ -22,7 +29,20 @@ func main() {
func newApp() *iris.Application {
app := iris.New()
pluralize := pluralize.NewClient()
// set the printers after load, so they can be done by loop of available languages.
printers := make(map[string]*message.Printer)
message.Set(language.Greek, "Hello %d dog",
plural.Selectf(1, "%d",
"one", "Γεια σου σκυλί",
"other", "Γεια σας %[1]d σκυλιά",
))
/* by variable, single word:
message.Set(language.Greek, "Hi %d dog(s)",
catalog.Var("dogs", plural.Selectf(1, "%d", "one", "σκυλί", "other", "σκυλιά")),
catalog.String("Γεια %[1]d ${dogs}"))
*/
// Set custom functions per locale!
app.I18n.Loader.Funcs = func(current iris.Locale) template.FuncMap {
@ -30,12 +50,7 @@ func newApp() *iris.Application {
"plural": func(word string, count int) string {
// Your own implementation or use a 3rd-party package
// like we do here.
//
// Note that this is only for english,
// but you can use the "current" locale
// and make a map with dictionaries to
// pluralize words based on the given language.
return pluralize.Pluralize(word, count, true)
return printers[current.Language()].Sprintf(word, count)
},
"uppercase": func(word string) string {
return strings.ToUpper(word)
@ -51,6 +66,12 @@ func newApp() *iris.Application {
panic(err)
}
for _, tag := range app.I18n.Tags() {
printers[tag.String()] = message.NewPrinter(tag)
}
message.NewPrinter(language.Greek).Printf("Hello %d dog", 2)
app.Get("/", func(ctx iris.Context) {
text := ctx.Tr("HiDogs", iris.Map{
"count": 2,

View File

@ -3,4 +3,3 @@ module app
go 1.15
require github.com/kataras/iris/v12 master

View File

@ -22,6 +22,7 @@ func main() {
// the .Name inside the ./templates/hi.html.
ctx.ViewData("Name", "iris")
// render the template with the file name relative to the './templates'.
// file extension is OPTIONAL.
ctx.View("hi.html")
})

View File

@ -12,14 +12,14 @@ func main() {
app.RegisterView(tmpl)
app.Get("/", func(ctx iris.Context) {
ctx.View("index.ace", iris.Map{
ctx.View("index", iris.Map{
"Title": "Title of The Page",
})
})
app.Get("/layout", func(ctx iris.Context) {
ctx.ViewLayout("layouts/main.ace") // layout for that response.
ctx.View("index.ace", iris.Map{
ctx.ViewLayout("layouts/main") // layout for that response.
ctx.View("index", iris.Map{ // file extension is optional.
"Title": "Title of the main Page",
})
})

View File

@ -88,6 +88,11 @@ type (
//
// It is an alias of the `context#XML` type.
XML = context.XML
// Markdown the optional settings for Markdown renderer.
// See `Context.Markdown` for more.
//
// It is an alias of the `context#Markdown` type.
Markdown = context.Markdown
// Supervisor is a shortcut of the `host#Supervisor`.
// Used to add supervisor configurators on common Runners
// without the need of importing the `core/host` package.

View File

@ -340,7 +340,7 @@ func WithCharset(charset string) Configurator {
// WithPostMaxMemory sets the maximum post data size
// that a client can send to the server, this differs
// from the overral request body size which can be modified
// from the overall request body size which can be modified
// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
//
// Defaults to 32MB or 32 << 20 or 32*iris.MB if you prefer.
@ -732,7 +732,7 @@ type Configuration struct {
// PostMaxMemory sets the maximum post data size
// that a client can send to the server, this differs
// from the overral request body size which can be modified
// from the overall request body size which can be modified
// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
//
// Defaults to 32MB or 32 << 20 if you prefer.

View File

@ -2491,6 +2491,12 @@ func (ctx *Context) Write(rawBody []byte) (int, error) {
//
// Returns the number of bytes written and any write error encountered.
func (ctx *Context) Writef(format string, a ...interface{}) (n int, err error) {
/* if len(a) == 0 {
return ctx.WriteString(format)
} ^ No, let it complain about arguments, because go test will do even if the app is running.
Users should use WriteString instead of (format, args)
when format may contain go-sprintf reserved chars (e.g. %).*/
return fmt.Fprintf(ctx.writer, format, a...)
}

View File

@ -4,11 +4,15 @@ import "io"
// ViewEngine is the interface which all view engines should be implemented in order to be registered inside iris.
type ViewEngine interface {
// Load should load the templates from a physical system directory or by an embedded one (assets/go-bindata).
// Name returns the name of the engine.
Name() string
// Load should load the templates from the given FileSystem.
Load() error
// ExecuteWriter should execute a template by its filename with an optional layout and bindingData.
ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error
// Ext should return the final file extension which this view engine is responsible to render.
// Ext should return the final file extension (including the dot)
// which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
Ext() string
}

View File

@ -1340,7 +1340,7 @@ func (api *APIBuilder) OnAnyErrorCode(handlers ...context.Handler) (routes []*Ro
return
}
// RegisterView registers and loads a view engine middleware for that group of routes.
// RegisterView registers and loads a view engine middleware for this group of routes.
// It overrides any of the application's root registered view engines.
// To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more.

View File

@ -294,8 +294,8 @@ type Party interface {
// Returns the GET *Route.
Favicon(favPath string, requestPath ...string) *Route
// RegisterView registers and loads a view engine middleware for that group of routes.
// It overrides any of the application's root registered view engines.
// RegisterView registers and loads a view engine middleware for this group of routes.
// It overrides any of the application's root registered view engine.
// To register a view engine per handler chain see the `Context.ViewEngine` instead.
// Read `Configuration.ViewEngineContextKey` documentation for more.
RegisterView(viewEngine context.ViewEngine)

View File

@ -73,6 +73,7 @@ type ReportEntry struct {
DependencyValue interface{} // the dependency value binded to that InputPosition of Name.
DependencyFile string // the file
DependencyLine int // and line number of the dependency's value.
Static bool
}
func (r *Report) fill(bindings []*binding) {
@ -101,6 +102,7 @@ func (r *Report) fill(bindings []*binding) {
DependencyValue: b.Dependency.OriginalValue,
DependencyFile: b.Dependency.Source.File,
DependencyLine: b.Dependency.Source.Line,
Static: b.Dependency.Static,
}
r.Entries = append(r.Entries, entry)

View File

@ -482,26 +482,6 @@ var _ Result = View{}
const dotB = byte('.')
// DefaultViewExt is the default extension if `view.Name `is missing,
// but note that it doesn't care about
// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext.
// so if you don't use the ".html" as extension for your files
// you have to append the extension manually into the `view.Name`
// or change this global variable.
var DefaultViewExt = ".html"
func ensureExt(s string) string {
if len(s) == 0 {
return "index" + DefaultViewExt
}
if strings.IndexByte(s, dotB) < 1 {
s += DefaultViewExt
}
return s
}
// Dispatch writes the template filename, template layout and (any) data to the client.
// Completes the `Result` interface.
func (r View) Dispatch(ctx *context.Context) { // r as Response view.
@ -514,10 +494,7 @@ func (r View) Dispatch(ctx *context.Context) { // r as Response view.
}
if r.Name != "" {
r.Name = ensureExt(r.Name)
if r.Layout != "" {
r.Layout = ensureExt(r.Layout)
ctx.ViewLayout(r.Layout)
}

View File

@ -118,6 +118,8 @@ func structFieldIgnored(f reflect.StructField) bool {
// all except non-zero.
func lookupFields(elem reflect.Value, skipUnexported bool, onlyZeros bool, parentIndex []int) (fields []reflect.StructField, stateless int) {
// Note: embedded pointers are not supported.
// elem = reflect.Indirect(elem)
elemTyp := elem.Type()
for i, n := 0, elem.NumField(); i < n; i++ {
field := elemTyp.Field(i)

View File

@ -9,6 +9,7 @@ import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
"github.com/kataras/iris/v12/i18n"
"github.com/iris-contrib/httpexpect/v2"
)
@ -158,6 +159,7 @@ var (
// For a more efficient testing please use the `New` function instead.
func Do(w http.ResponseWriter, r *http.Request, handler iris.Handler, irisConfigurators ...iris.Configurator) {
app := new(iris.Application)
app.I18n = i18n.New()
app.Configure(iris.WithConfiguration(iris.DefaultConfiguration()), iris.WithLogLevel("disable"))
app.Configure(irisConfigurators...)

View File

@ -11,11 +11,25 @@ import (
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/router"
"github.com/kataras/iris/v12/i18n/internal"
"golang.org/x/text/language"
)
type (
// MessageFunc is the function type to modify the behavior when a key or language was not found.
// All language inputs fallback to the default locale if not matched.
// This is why this signature accepts both input and matched languages, so caller
// can provide better messages.
//
// The first parameter is set to the client real input of the language,
// the second one is set to the matched language (default one if input wasn't matched)
// and the third and forth are the translation format/key and its optional arguments.
//
// Note: we don't accept the Context here because Tr method and template func {{ tr }}
// have no direct access to it.
MessageFunc = internal.MessageFunc
// Loader accepts a `Matcher` and should return a `Localizer`.
// Functions that implement this type should load locale files.
Loader func(m *Matcher) (Localizer, error)
@ -29,19 +43,6 @@ type (
// It may return the default language if nothing else matches based on custom localizer's criteria.
GetLocale(index int) context.Locale
}
// MessageFunc is the function type to modify the behavior when a key or language was not found.
// All language inputs fallback to the default locale if not matched.
// This is why this signature accepts both input and matched languages, so caller
// can provide better messages.
//
// The first parameter is set to the client real input of the language,
// the second one is set to the matched language (default one if input wasn't matched)
// and the third and forth are the translation format/key and its optional arguments.
//
// Note: we don't accept the Context here because Tr method and template func {{ tr }}
// have no direct access to it.
MessageFunc func(langInput, langMatched, key string, args ...interface{}) string
)
// I18n is the structure which keeps the i18n configuration and implements localization and internationalization features.
@ -49,7 +50,7 @@ type I18n struct {
localizer Localizer
matcher *Matcher
Loader *LoaderConfig
Loader LoaderConfig
loader Loader
mu sync.Mutex
@ -106,13 +107,10 @@ func makeTags(languages ...string) (tags []language.Tag) {
}
// New returns a new `I18n` instance. Use its `Load` or `LoadAssets` to load languages.
// Examples at: https://github.com/kataras/iris/tree/master/_examples/i18n.
func New() *I18n {
i := &I18n{
Loader: &LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
},
Loader: DefaultLoaderConfig,
URLParameter: "lang",
Subdomain: true,
PathRedirect: true,

5
i18n/internal/aliases.go Normal file
View File

@ -0,0 +1,5 @@
package internal
// Map is just an alias of the map[string]interface{} type.
// Just like the iris.Map one.
type Map = map[string]interface{}

149
i18n/internal/catalog.go Normal file
View File

@ -0,0 +1,149 @@
package internal
import (
"fmt"
"text/template"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// MessageFunc is the function type to modify the behavior when a key or language was not found.
// All language inputs fallback to the default locale if not matched.
// This is why this signature accepts both input and matched languages, so caller
// can provide better messages.
//
// The first parameter is set to the client real input of the language,
// the second one is set to the matched language (default one if input wasn't matched)
// and the third and forth are the translation format/key and its optional arguments.
//
// Note: we don't accept the Context here because Tr method and template func {{ tr }}
// have no direct access to it.
type MessageFunc func(langInput, langMatched, key string, args ...interface{}) string
// Catalog holds the locales and the variables message storage.
type Catalog struct {
builder *catalog.Builder
Locales []*Locale
}
// The Options of the Catalog and its Locales.
type Options struct {
// Left delimiter for template messages.
Left string
// Right delimeter for template messages.
Right string
// Enable strict mode.
Strict bool
// Optional functions for template messages per locale.
Funcs func(context.Locale) template.FuncMap
// Optional function to be called when no message was found.
DefaultMessageFunc MessageFunc
// Customize the overall behavior of the plurazation feature.
PluralFormDecoder PluralFormDecoder
}
// NewCatalog returns a new Catalog based on the registered languages and the loader options.
func NewCatalog(languages []language.Tag, opts Options) (*Catalog, error) { // ordered languages, the first should be the default one.
if len(languages) == 0 {
return nil, fmt.Errorf("catalog: empty languages")
}
if opts.Left == "" {
opts.Left = "{{"
}
if opts.Right == "" {
opts.Right = "}}"
}
if opts.PluralFormDecoder == nil {
opts.PluralFormDecoder = DefaultPluralFormDecoder
}
builder := catalog.NewBuilder(catalog.Fallback(languages[0]))
locales := make([]*Locale, 0, len(languages))
for idx, tag := range languages {
locale := &Locale{
tag: tag,
index: idx,
ID: tag.String(),
Options: opts,
Printer: message.NewPrinter(tag, message.Catalog(builder)),
Messages: make(map[string]Renderer),
}
locale.FuncMap = getFuncs(locale)
locales = append(locales, locale)
}
c := &Catalog{
builder: builder,
Locales: locales,
}
return c, nil
}
// Set sets a simple translation message.
func (c *Catalog) Set(tag language.Tag, key string, msgs ...catalog.Message) error {
// fmt.Printf("Catalog.Set[%s] %s:\n", tag.String(), key)
// for _, msg := range msgs {
// fmt.Printf("%#+v\n", msg)
// }
return c.builder.Set(tag, key, msgs...)
}
// Store stores the a map of values to the locale derives from the given "langIndex".
func (c *Catalog) Store(langIndex int, kv Map) error {
loc := c.getLocale(langIndex)
if loc == nil {
return fmt.Errorf("expected language index to be lower or equal than %d but got %d", len(c.Locales), langIndex)
}
return loc.Load(c, kv)
}
/* Localizer interface. */
// SetDefault changes the default language based on the "index".
// See `I18n#SetDefault` method for more.
func (c *Catalog) SetDefault(index int) bool {
if index < 0 {
index = 0
}
if maxIdx := len(c.Locales) - 1; index > maxIdx {
return false
}
// callers should protect with mutex if called at serve-time.
loc := c.Locales[index]
loc.index = 0
f := c.Locales[0]
c.Locales[0] = loc
f.index = index
c.Locales[index] = f
return true
}
// GetLocale returns a valid `Locale` based on the "index".
func (c *Catalog) GetLocale(index int) context.Locale {
return c.getLocale(index)
}
func (c *Catalog) getLocale(index int) *Locale {
if index < 0 {
index = 0
}
if maxIdx := len(c.Locales) - 1; index > maxIdx {
// panic("expected language index to be lower or equal than %d but got %d", maxIdx, langIndex)
return nil
}
loc := c.Locales[index]
return loc
}

195
i18n/internal/locale.go Normal file
View File

@ -0,0 +1,195 @@
package internal
import (
"fmt"
"text/template"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// Locale is the default Locale.
// Created by Catalog.
// One Locale maps to one registered and loaded language.
// Stores the translation variables and most importantly, the Messages (keys and their renderers).
type Locale struct {
// The index of the language registered by the user, starting from zero.
index int
tag language.Tag
// ID is the tag.String().
ID string
// Options given by the Catalog
Options Options
// Fields set by Catalog.
FuncMap template.FuncMap
Printer *message.Printer
//
// Fields set by this Load method.
Messages map[string]Renderer
Vars []Var // shared per-locale variables.
}
// Ensures that the Locale completes the context.Locale interface.
var _ context.Locale = (*Locale)(nil)
// Load sets the translation messages based on the Catalog's key values.
func (loc *Locale) Load(c *Catalog, keyValues Map) error {
return loc.setMap(c, "", keyValues)
}
func (loc *Locale) setMap(c *Catalog, key string, keyValues Map) error {
// unique locals or the shared ones.
isRoot := key == ""
vars := getVars(loc, VarsKey, keyValues)
if isRoot {
loc.Vars = vars
} else {
vars = removeVarsDuplicates(append(vars, loc.Vars...))
}
for k, v := range keyValues {
form, isPlural := loc.Options.PluralFormDecoder(loc, k)
if isPlural {
k = key
} else if !isRoot {
k = key + "." + k
}
switch value := v.(type) {
case string:
if err := loc.setString(c, k, value, vars, form); err != nil {
return fmt.Errorf("%s:%s parse string: %w", loc.ID, key, err)
}
case Map:
// fmt.Printf("%s is map\n", fullKey)
if err := loc.setMap(c, k, value); err != nil {
return fmt.Errorf("%s:%s parse map: %w", loc.ID, key, err)
}
default:
return fmt.Errorf("%s:%s unexpected type of %T as value", loc.ID, key, value)
}
}
return nil
}
func (loc *Locale) setString(c *Catalog, key string, value string, vars []Var, form PluralForm) (err error) {
isPlural := form != nil
// fmt.Printf("setStringVars: %s=%s\n", key, value)
msgs, vars := makeSelectfVars(value, vars, isPlural)
msgs = append(msgs, catalog.String(value))
m := &Message{
Locale: loc,
Key: key,
Value: value,
Vars: vars,
Plural: isPlural,
}
var (
renderer, pluralRenderer Renderer = m, m
)
if stringIsTemplateValue(value, loc.Options.Left, loc.Options.Right) {
t, err := NewTemplate(c, m)
if err != nil {
return err
}
pluralRenderer = t
if !isPlural {
renderer = t
}
} else {
if isPlural {
pluralRenderer, err = newIndependentPluralRenderer(c, loc, key, msgs...)
if err != nil {
return fmt.Errorf("<%s = %s>: %w", key, value, err)
}
} else {
// let's make normal keys direct fire:
// renderer = &simpleRenderer{key, loc.Printer}
if err = c.Set(loc.tag, key, msgs...); err != nil {
return fmt.Errorf("<%s = %s>: %w", key, value, err)
}
}
}
if isPlural {
if existingMsg, ok := loc.Messages[key]; ok {
if msg, ok := existingMsg.(*Message); ok {
msg.AddPlural(form, pluralRenderer)
return
}
}
m.AddPlural(form, pluralRenderer)
}
loc.Messages[key] = renderer
return
}
/* context.Locale interface */
// Index returns the current locale index from the languages list.
func (loc *Locale) Index() int {
return loc.index
}
// Tag returns the full language Tag attached to this Locale,
// it should be unique across different Locales.
func (loc *Locale) Tag() *language.Tag {
return &loc.tag
}
// Language should return the exact languagecode of this `Locale`
//that the user provided on `New` function.
//
// Same as `Tag().String()` but it's static.
func (loc *Locale) Language() string {
return loc.ID
}
// GetMessage should return translated text based on the given "key".
func (loc *Locale) GetMessage(key string, args ...interface{}) string {
return loc.getMessage(loc.ID, key, args...)
}
// GetMessageContext same as GetMessage
// but it accepts the Context as its first input.
// If DefaultMessageFunc was not nil then this Context
// will provide the real language input instead of the locale's which
// may be the default language one.
func (loc *Locale) GetMessageContext(ctx *context.Context, key string, args ...interface{}) string {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
return loc.getMessage(langInput, key, args...)
}
func (loc *Locale) getMessage(langInput, key string, args ...interface{}) string {
if msg, ok := loc.Messages[key]; ok {
result, err := msg.Render(args...)
if err != nil {
result = err.Error()
}
return result
}
if fn := loc.Options.DefaultMessageFunc; fn != nil {
// let langInput to be empty if that's the case.
return fn(langInput, loc.ID, key, args...)
}
return ""
}

81
i18n/internal/message.go Normal file
View File

@ -0,0 +1,81 @@
package internal
import (
"fmt"
"sort"
)
// Renderer is responsible to render a translation based
// on the given "args".
type Renderer interface {
Render(args ...interface{}) (string, error)
}
// Message is the default Renderer for translation messages.
// Holds the variables and the plurals of this key.
// Each Locale has its own list of messages.
type Message struct {
Locale *Locale
Key string
Value string
Plural bool
Plurals []*PluralMessage // plural forms by order.
Vars []Var
}
// AddPlural adds a plural message to the Plurals list.
func (m *Message) AddPlural(form PluralForm, r Renderer) {
msg := &PluralMessage{
Form: form,
Renderer: r,
}
if len(m.Plurals) == 0 {
m.Plural = true
m.Plurals = append(m.Plurals, msg)
return
}
for i, p := range m.Plurals {
if p.Form.String() == form.String() {
// replace
m.Plurals[i] = msg
return
}
}
m.Plurals = append(m.Plurals, msg)
sort.SliceStable(m.Plurals, func(i, j int) bool {
return m.Plurals[i].Form.Less(m.Plurals[j].Form)
})
}
// Render completes the Renderer interface.
// It accepts arguments, which can resolve the pluralization type of the message
// and its variables. If the Message is wrapped by a Template then the
// first argument should be a map. The map key resolves to the pluralization
// of the message is the "PluralCount". And for variables the user
// should set a message key which looks like: %VAR_NAME%Count, e.g. "DogsCount"
// to set plural count for the "Dogs" variable, case-sensitive.
func (m *Message) Render(args ...interface{}) (string, error) {
if m.Plural {
if len(args) > 0 {
if pluralCount, ok := findPluralCount(args[0]); ok {
for _, plural := range m.Plurals {
if plural.Form.MatchPlural(pluralCount) {
return plural.Renderer.Render(args...)
}
}
return "", fmt.Errorf("key: %q: no registered plurals for <%d>", m.Key, pluralCount)
}
}
return "", fmt.Errorf("key: %q: missing plural count argument", m.Key)
}
return m.Locale.Printer.Sprintf(m.Key, args...), nil
}

261
i18n/internal/plural.go Normal file
View File

@ -0,0 +1,261 @@
package internal
import (
"strconv"
"github.com/kataras/iris/v12/context"
"golang.org/x/text/feature/plural"
"golang.org/x/text/message"
"golang.org/x/text/message/catalog"
)
// PluralCounter if completes by an input argument of a message to render,
// then the plural renderer will resolve the plural count
// and any variables' counts. This is useful when the data is not a type of Map or integers.
type PluralCounter interface {
// PluralCount returns the plural count of the message.
// If returns -1 then this is not a valid plural message.
PluralCount() int
// VarCount should return the variable count, based on the variable name.
VarCount(name string) int
}
// PluralMessage holds the registered Form and the corresponding Renderer.
// It is used on the `Message.AddPlural` method.
type PluralMessage struct {
Form PluralForm
Renderer Renderer
}
type independentPluralRenderer struct {
key string
printer *message.Printer
}
func newIndependentPluralRenderer(c *Catalog, loc *Locale, key string, msgs ...catalog.Message) (Renderer, error) {
builder := catalog.NewBuilder(catalog.Fallback(c.Locales[0].tag))
if err := builder.Set(loc.tag, key, msgs...); err != nil {
return nil, err
}
printer := message.NewPrinter(loc.tag, message.Catalog(builder))
return &independentPluralRenderer{key, printer}, nil
}
func (m *independentPluralRenderer) Render(args ...interface{}) (string, error) {
return m.printer.Sprintf(m.key, args...), nil
}
// A PluralFormDecoder should report and return whether
// a specific "key" is a plural one. This function
// can be implemented and set on the `Options` to customize
// the plural forms and their behavior in general.
//
// See the `DefaultPluralFormDecoder` package-level
// variable for the default implementation one.
type PluralFormDecoder func(loc context.Locale, key string) (PluralForm, bool)
// DefaultPluralFormDecoder is the default `PluralFormDecoder`.
// Supprots "zero", "one", "two", "other", "=x", "<x", ">x".
var DefaultPluralFormDecoder = func(_ context.Locale, key string) (PluralForm, bool) {
if isDefaultPluralForm(key) {
return pluralForm(key), true
}
return nil, false
}
func isDefaultPluralForm(s string) bool {
switch s {
case "zero", "one", "two", "other":
return true
default:
if len(s) > 1 {
ch := s[0]
if ch == '=' || ch == '<' || ch == '>' {
if isDigit(s[1]) {
return true
}
}
}
return false
}
}
// A PluralForm is responsible to decode
// locale keys to plural forms and match plural forms
// based on the given pluralCount.
//
// See `pluralForm` package-level type for a default implementation.
type PluralForm interface {
String() string
// the string is a verified plural case's raw string value.
// Field for priority on which order to register the plural cases.
Less(next PluralForm) bool
MatchPlural(pluralCount int) bool
}
type pluralForm string
func (f pluralForm) String() string {
return string(f)
}
func (f pluralForm) Less(next PluralForm) bool {
form1 := f.String()
form2 := next.String()
// Order by
// - equals,
// - less than
// - greater than
// - "zero", "one", "two"
// - rest is last "other".
dig1, typ1, hasDig1 := formAtoi(form1)
if typ1 == eq {
return true
}
dig2, typ2, hasDig2 := formAtoi(form2)
if typ2 == eq {
return false
}
// digits smaller, number.
if hasDig1 {
return !hasDig2 || dig1 < dig2
}
if hasDig2 {
return false
}
if form1 == "other" {
return false // other go to last.
}
if form2 == "other" {
return true
}
if form1 == "zero" {
return true
}
if form2 == "zero" {
return false
}
if form1 == "one" {
return true
}
if form2 == "one" {
return false
}
if form1 == "two" {
return true
}
if form2 == "two" {
return false
}
return false
}
func (f pluralForm) MatchPlural(pluralCount int) bool {
switch f {
case "other":
return true
case "=0", "zero":
return pluralCount == 0
case "=1", "one":
return pluralCount == 1
case "=2", "two":
return pluralCount == 2
default:
// <5 or =5
n, typ, ok := formAtoi(string(f))
if !ok {
return false
}
switch typ {
case eq:
return n == pluralCount
case lt:
return pluralCount < n
case gt:
return pluralCount > n
default:
return false
}
}
}
func makeSelectfVars(text string, vars []Var, insidePlural bool) ([]catalog.Message, []Var) {
newVars := sortVars(text, vars)
newVars = removeVarsDuplicates(newVars)
msgs := selectfVars(newVars, insidePlural)
return msgs, newVars
}
func selectfVars(vars []Var, insidePlural bool) []catalog.Message {
msgs := make([]catalog.Message, 0, len(vars))
for _, variable := range vars {
argth := variable.Argth
if insidePlural {
argth++
}
msg := catalog.Var(variable.Name, plural.Selectf(argth, variable.Format, variable.Cases...))
// fmt.Printf("%s:%d | cases | %#+v\n", variable.Name, variable.Argth, variable.Cases)
msgs = append(msgs, msg)
}
return msgs
}
const (
eq uint8 = iota + 1
lt
gt
)
func formType(ch byte) uint8 {
switch ch {
case '=':
return eq
case '<':
return lt
case '>':
return gt
}
return 0
}
func formAtoi(form string) (int, uint8, bool) {
if len(form) < 2 {
return -1, 0, false
}
typ := formType(form[0])
if typ == 0 {
return -1, 0, false
}
dig, err := strconv.Atoi(form[1:])
if err != nil {
return -1, 0, false
}
return dig, typ, true
}
func isDigit(ch byte) bool {
return '0' <= ch && ch <= '9'
}

242
i18n/internal/template.go Normal file
View File

@ -0,0 +1,242 @@
package internal
import (
"bytes"
"fmt"
"strconv"
"strings"
"sync"
"text/template"
"golang.org/x/text/message/catalog"
)
const (
// VarsKey is the key for the message's variables, per locale(global) or per key (local).
VarsKey = "Vars"
// PluralCountKey is the key for the template's message pluralization.
PluralCountKey = "PluralCount"
// VarCountKeySuffix is the key suffix for the template's variable's pluralization,
// e.g. HousesCount for ${Houses}.
VarCountKeySuffix = "Count"
// VarsKeySuffix is the key which the template message's variables
// are stored with,
// e.g. welcome.human.other_vars
VarsKeySuffix = "_vars"
)
// Template is a Renderer which renders template messages.
type Template struct {
*Message
tmpl *template.Template
bufPool *sync.Pool
}
// NewTemplate returns a new Template message based on the
// catalog and the base translation Message. See `Locale.Load` method.
func NewTemplate(c *Catalog, m *Message) (*Template, error) {
tmpl, err := template.New(m.Key).
Delims(m.Locale.Options.Left, m.Locale.Options.Right).
Funcs(m.Locale.FuncMap).
Parse(m.Value)
if err != nil {
return nil, err
}
if err := registerTemplateVars(c, m); err != nil {
return nil, fmt.Errorf("template vars: <%s = %s>: %w", m.Key, m.Value, err)
}
bufPool := &sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
t := &Template{
Message: m,
tmpl: tmpl,
bufPool: bufPool,
}
return t, nil
}
func registerTemplateVars(c *Catalog, m *Message) error {
if len(m.Vars) == 0 {
return nil
}
msgs := selectfVars(m.Vars, false)
variableText := ""
for _, variable := range m.Vars {
variableText += variable.Literal + " "
}
variableText = variableText[0 : len(variableText)-1]
fullKey := m.Key + "." + VarsKeySuffix
return c.Set(m.Locale.tag, fullKey, append(msgs, catalog.String(variableText))...)
}
// Render completes the Renderer interface.
// It renders a template message.
// Each key has its own Template, plurals too.
func (t *Template) Render(args ...interface{}) (string, error) {
var (
data interface{}
result string
)
argsLength := len(args)
if argsLength > 0 {
data = args[0]
}
buf := t.bufPool.Get().(*bytes.Buffer)
buf.Reset()
if err := t.tmpl.Execute(buf, data); err != nil {
t.bufPool.Put(buf)
return "", err
}
result = buf.String()
t.bufPool.Put(buf)
if len(t.Vars) > 0 {
// get the variables plurals.
if argsLength > 1 {
// if has more than the map/struct
// then let's assume the user passes variable counts by raw integer arguments.
args = args[1:]
} else if data != nil {
// otherwise try to resolve them by the map(%var_name%Count)/struct(PlrualCounter).
args = findVarsCount(data, t.Vars)
}
result = t.replaceTmplVars(result, args...)
}
return result, nil
}
func findVarsCount(data interface{}, vars []Var) (args []interface{}) {
if data == nil {
return nil
}
switch dataValue := data.(type) {
case PluralCounter:
for _, v := range vars {
if count := dataValue.VarCount(v.Name); count >= 0 {
args = append(args, count)
}
}
case Map:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
args = append(args, value)
}
}
case map[string]string:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
if count, err := strconv.Atoi(value); err == nil {
args = append(args, count)
}
}
}
case map[string]int:
for _, v := range vars {
varCountKey := v.Name + VarCountKeySuffix
if value, ok := dataValue[varCountKey]; ok {
args = append(args, value)
}
}
default:
return nil
}
return
}
func findPluralCount(data interface{}) (int, bool) {
if data == nil {
return -1, false
}
switch dataValue := data.(type) {
case PluralCounter:
if count := dataValue.PluralCount(); count >= 0 {
return count, true
}
case Map:
if v, ok := dataValue[PluralCountKey]; ok {
if count, ok := v.(int); ok {
return count, true
}
}
case map[string]string:
if v, ok := dataValue[PluralCountKey]; ok {
count, err := strconv.Atoi(v)
if err != nil {
return -1, false
}
return count, true
}
case map[string]int:
if count, ok := dataValue[PluralCountKey]; ok {
return count, true
}
case int:
return dataValue, true // when this is not a template data, the caller's argument should be args[1:] now.
case int64:
count := int(dataValue)
return count, true
}
return -1, false
}
func (t *Template) replaceTmplVars(result string, args ...interface{}) string {
varsKey := t.Key + "." + VarsKeySuffix
translationVarsText := t.Locale.Printer.Sprintf(varsKey, args...)
if translationVarsText != "" {
translatioVars := strings.Split(translationVarsText, " ")
for i, variable := range t.Vars {
result = strings.Replace(result, variable.Literal, translatioVars[i], 1)
}
}
return result
}
func stringIsTemplateValue(value, left, right string) bool {
leftIdx, rightIdx := strings.Index(value, left), strings.Index(value, right)
return leftIdx != -1 && rightIdx > leftIdx
}
func getFuncs(loc *Locale) template.FuncMap {
// set the template funcs for this locale.
funcs := template.FuncMap{
"tr": loc.GetMessage,
}
if getFuncs := loc.Options.Funcs; getFuncs != nil {
// set current locale's template's funcs.
for k, v := range getFuncs(loc) {
funcs[k] = v
}
}
return funcs
}

182
i18n/internal/var.go Normal file
View File

@ -0,0 +1,182 @@
package internal
import (
"reflect"
"regexp"
"sort"
"golang.org/x/text/message/catalog"
)
// Var represents a message variable.
// The variables, like the sub messages are sorted.
// First: plurals (which again, are sorted)
// and then any custom keys.
// In variables, the sorting depends on the exact
// order the associated message uses the variables.
// This is extremely handy.
// This package requires the golang.org/x/text/message capabilities
// only for the variables feature, the message itself's pluralization is managed by the package.
type Var struct {
Name string // Variable name, e.g. Name
Literal string // Its literal is ${Name}
Cases []interface{} // one:...,few:...,...
Format string // defaults to "%d".
Argth int // 1, 2, 3...
}
func getVars(loc *Locale, key string, src map[string]interface{}) []Var {
if len(src) == 0 {
return nil
}
varsKey, ok := src[key]
if !ok {
return nil
}
varValue, ok := varsKey.([]interface{})
if !ok {
return nil
}
vars := make([]Var, 0, len(varValue))
for _, v := range varValue {
m, ok := v.(map[string]interface{})
if !ok {
continue
}
for k, inner := range m {
varFormat := "%d"
innerMap, ok := inner.(map[string]interface{})
if !ok {
continue
}
for kk, vv := range innerMap {
if kk == "format" {
if format, ok := vv.(string); ok {
varFormat = format
}
break
}
}
cases := getCases(loc, innerMap)
if len(cases) > 0 {
// cases = sortCases(cases)
vars = append(vars, Var{
Name: k,
Literal: "${" + k + "}",
Cases: cases,
Format: varFormat,
Argth: 1,
})
}
}
}
delete(src, key) // delete the key after.
return vars
}
var unescapeVariableRegex = regexp.MustCompile("\\$\\{(.*?)}")
func sortVars(text string, vars []Var) (newVars []Var) {
argth := 1
for _, submatches := range unescapeVariableRegex.FindAllStringSubmatch(text, -1) {
name := submatches[1]
for _, variable := range vars {
if variable.Name == name {
variable.Argth = argth
newVars = append(newVars, variable)
argth++
break
}
}
}
sort.SliceStable(newVars, func(i, j int) bool {
return newVars[i].Argth < newVars[j].Argth
})
return
}
// it will panic if the incoming "elements" are not catmsg.Var (internal text package).
func removeVarsDuplicates(elements []Var) (result []Var) {
seen := make(map[string]struct{})
for v := range elements {
variable := elements[v]
name := variable.Name
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
result = append(result, variable)
}
}
return result
}
func removeMsgVarsDuplicates(elements []catalog.Message) (result []catalog.Message) {
seen := make(map[string]struct{})
for _, elem := range elements {
val := reflect.Indirect(reflect.ValueOf(elem))
if val.Type().String() != "catmsg.Var" {
// keep.
result = append(result, elem)
continue // it's not a var.
}
name := val.FieldByName("Name").Interface().(string)
if _, ok := seen[name]; !ok {
seen[name] = struct{}{}
result = append(result, elem)
}
}
return
}
func getCases(loc *Locale, src map[string]interface{}) []interface{} {
type PluralCase struct {
Form PluralForm
Value interface{}
}
pluralCases := make([]PluralCase, 0, len(src))
for key, value := range src {
form, ok := loc.Options.PluralFormDecoder(loc, key)
if !ok {
continue
}
pluralCases = append(pluralCases, PluralCase{
Form: form,
Value: value,
})
}
if len(pluralCases) == 0 {
return nil
}
sort.SliceStable(pluralCases, func(i, j int) bool {
left, right := pluralCases[i].Form, pluralCases[j].Form
return left.Less(right)
})
cases := make([]interface{}, 0, len(pluralCases)*2)
for _, pluralCase := range pluralCases {
// fmt.Printf("%s=%v\n", pluralCase.Form, pluralCase.Value)
cases = append(cases, pluralCase.Form.String())
cases = append(cases, pluralCase.Value)
}
return cases
}

View File

@ -1,64 +1,24 @@
package i18n
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"sync"
"text/template"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/i18n/internal"
"github.com/BurntSushi/toml"
"golang.org/x/text/language"
"gopkg.in/ini.v1"
"gopkg.in/yaml.v3"
)
// LoaderConfig is an optional configuration structure which contains
// LoaderConfig the configuration structure which contains
// some options about how the template loader should act.
//
// See `Glob` and `Assets` package-level functions.
type (
LoaderConfig struct {
// Template delimiters, defaults to {{ }}.
Left, Right string
// Template functions map per locale, defaults to nil.
Funcs func(context.Locale) template.FuncMap
// If true then it will return error on invalid templates instead of moving them to simple string-line keys.
// Also it will report whether the registered languages matched the loaded ones.
// Defaults to false.
Strict bool
}
// LoaderOption is a type which accepts a pointer to `LoaderConfig`
// and can be optionally passed to the second
// variadic input argument of the `Glob` and `Assets` functions.
LoaderOption interface {
Apply(*LoaderConfig)
}
)
// Apply implements the `LoaderOption` interface.
func (c *LoaderConfig) Apply(cfg *LoaderConfig) {
if c.Left != "" {
cfg.Left = c.Left
}
if c.Right != "" {
cfg.Right = c.Right
}
if c.Funcs != nil {
cfg.Funcs = c.Funcs
}
if c.Strict {
cfg.Strict = true
}
}
type LoaderConfig = internal.Options
// Glob accepts a glob pattern (see: https://golang.org/pkg/path/filepath/#Glob)
// and loads the locale files based on any "options".
@ -67,13 +27,13 @@ func (c *LoaderConfig) Apply(cfg *LoaderConfig) {
// search and load for locale files.
//
// See `New` and `LoaderConfig` too.
func Glob(globPattern string, options ...LoaderOption) Loader {
func Glob(globPattern string, options LoaderConfig) Loader {
assetNames, err := filepath.Glob(globPattern)
if err != nil {
panic(err)
}
return load(assetNames, ioutil.ReadFile, options...)
return load(assetNames, ioutil.ReadFile, options)
}
// Assets accepts a function that returns a list of filenames (physical or virtual),
@ -82,8 +42,18 @@ func Glob(globPattern string, options ...LoaderOption) Loader {
// It returns a valid `Loader` which loads and maps the locale files.
//
// See `Glob`, `Assets`, `New` and `LoaderConfig` too.
func Assets(assetNames func() []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
return load(assetNames(), asset, options...)
func Assets(assetNames func() []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
return load(assetNames(), asset, options)
}
// DefaultLoaderConfig represents the default loader configuration.
var DefaultLoaderConfig = LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
DefaultMessageFunc: nil,
PluralFormDecoder: internal.DefaultPluralFormDecoder,
Funcs: nil,
}
// load accepts a list of filenames (physical or virtual),
@ -92,24 +62,21 @@ func Assets(assetNames func() []string, asset func(string) ([]byte, error), opti
// It returns a valid `Loader` which loads and maps the locale files.
//
// See `Glob`, `Assets` and `LoaderConfig` too.
func load(assetNames []string, asset func(string) ([]byte, error), options ...LoaderOption) Loader {
var c = LoaderConfig{
Left: "{{",
Right: "}}",
Strict: false,
}
for _, opt := range options {
opt.Apply(&c)
}
func load(assetNames []string, asset func(string) ([]byte, error), options LoaderConfig) Loader {
return func(m *Matcher) (Localizer, error) {
languageFiles, err := m.ParseLanguageFiles(assetNames)
if err != nil {
return nil, err
}
locales := make(MemoryLocalizer)
if options.DefaultMessageFunc == nil {
options.DefaultMessageFunc = m.defaultMessageFunc
}
cat, err := internal.NewCatalog(m.Languages, options)
if err != nil {
return nil, err
}
for langIndex, langFiles := range languageFiles {
keyValues := make(map[string]interface{})
@ -137,213 +104,22 @@ func load(assetNames []string, asset func(string) ([]byte, error), options ...Lo
}
}
var (
templateKeys = make(map[string]*template.Template)
lineKeys = make(map[string]string)
other = make(map[string]interface{})
)
t := m.Languages[langIndex]
locale := &defaultLocale{
index: langIndex,
id: t.String(),
tag: &t,
templateKeys: templateKeys,
lineKeys: lineKeys,
other: other,
defaultMessageFunc: m.defaultMessageFunc,
err = cat.Store(langIndex, keyValues)
if err != nil {
return nil, err
}
var longestValueLength int
for k, v := range keyValues {
// fmt.Printf("[%d] %s = %v of type: [%T]\n", langIndex, k, v, v)
switch value := v.(type) {
case string:
if leftIdx, rightIdx := strings.Index(value, c.Left), strings.Index(value, c.Right); leftIdx != -1 && rightIdx > leftIdx {
// we assume it's template?
// each file:line has its own template funcs so,
// just map it.
// builtin funcs.
funcs := template.FuncMap{
"tr": locale.GetMessage,
}
if c.Funcs != nil {
// set current locale's template's funcs.
for k, v := range c.Funcs(locale) {
funcs[k] = v
}
}
t, err := template.New(k).Delims(c.Left, c.Right).Funcs(funcs).Parse(value)
if err == nil {
templateKeys[k] = t
} else if c.Strict {
return nil, err
}
if valueLength := len(value); valueLength > longestValueLength {
longestValueLength = valueLength
}
}
lineKeys[k] = value
default:
other[k] = v
}
}
// pre-allocate the initial internal buffer.
// Note that Reset should be called immediately.
initBuf := []byte(strings.Repeat("x", longestValueLength))
locale.tmplBufPool = &sync.Pool{
New: func() interface{} {
// try to eliminate the internal "grow" method as much as possible.
return bytes.NewBuffer(initBuf)
},
}
locales[langIndex] = locale
}
if n := len(locales); n == 0 {
if n := len(cat.Locales); n == 0 {
return nil, fmt.Errorf("locales not found in %s", strings.Join(assetNames, ", "))
} else if c.Strict && n < len(m.Languages) {
} 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 locales, nil
return cat, nil
}
}
// MemoryLocalizer is a map which implements the `Localizer`.
type MemoryLocalizer map[int]context.Locale
// GetLocale returns a valid `Locale` based on the "index".
func (l MemoryLocalizer) GetLocale(index int) context.Locale {
// loc, ok := l[index]
// if !ok {
// panic(fmt.Sprintf("locale of index [%d] not found", index))
// }
// return loc
/* Note(@kataras): the following is allowed as a language index can be higher
than the length of the locale files.
if index >= len(l) || index < 0 {
// 1. language exists in the caller but was not found in files.
// 2. language exists in both files and caller but the actual
// languages are two, while the registered are 4 (when missing files),
// that happens when Strict option is false.
// force to the default language but what is the default language if the language index is greater than this length?
// That's why it's allowed.
index = 0
}*/
if index < 0 {
index = 0
}
if locale, ok := l[index]; ok {
return locale
}
return l[0]
}
// SetDefault changes the default language based on the "index".
// See `I18n#SetDefault` method for more.
func (l MemoryLocalizer) SetDefault(index int) bool {
// callers should protect with mutex if called at serve-time.
if loc, ok := l[index]; ok {
f := l[0]
l[0] = loc
l[index] = f
return true
}
return false
}
type defaultLocale struct {
index int
id string
tag *language.Tag
// templates *template.Template // we could use the ExecuteTemplate too.
templateKeys map[string]*template.Template
lineKeys map[string]string
other map[string]interface{}
defaultMessageFunc MessageFunc
tmplBufPool *sync.Pool
}
func (l *defaultLocale) Index() int {
return l.index
}
func (l *defaultLocale) Tag() *language.Tag {
return l.tag
}
func (l *defaultLocale) Language() string {
return l.id
}
func (l *defaultLocale) GetMessage(key string, args ...interface{}) string {
return l.getMessage(l.id, key, args...)
}
func (l *defaultLocale) GetMessageContext(ctx *context.Context, key string, args ...interface{}) string {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
return l.getMessage(langInput, key, args...)
}
func (l *defaultLocale) getMessage(langInput, key string, args ...interface{}) string {
// search on templates.
if tmpl, ok := l.templateKeys[key]; ok {
var (
data interface{}
text string
)
if len(args) > 0 {
data = args[0]
}
buf := l.tmplBufPool.Get().(*bytes.Buffer)
buf.Reset()
err := tmpl.Execute(buf, data)
if err != nil {
text = err.Error()
} else {
text = buf.String()
}
l.tmplBufPool.Put(buf)
return text
}
if text, ok := l.lineKeys[key]; ok {
return fmt.Sprintf(text, args...)
}
n := len(args)
if v, ok := l.other[key]; ok {
if n > 0 {
return fmt.Sprintf("%v [%v]", v, args)
}
return fmt.Sprintf("%v", v)
}
if l.defaultMessageFunc != nil {
// let langInput to be empty if that's the case.
return l.defaultMessageFunc(langInput, l.id, key, args...)
}
return ""
}
func unmarshalINI(data []byte, v interface{}) error {
f, err := ini.Load(data)
if err != nil {

22
iris.go
View File

@ -151,7 +151,9 @@ func Default() *Application {
})
})
app.UseRouter(accesslog.New(logFile).Handler)
ac := accesslog.New(logFile)
ac.AddOutput(app.logger.Printer)
app.UseRouter(ac.Handler)
app.logger.Debugf("Using <%s> to log requests", logFile.Name())
}
@ -385,8 +387,9 @@ func (app *Application) Minifier() *minify.M {
return app.minifier
}
// RegisterView should be used to register view engines mapping to a root directory
// and the template file(s) extension.
// RegisterView registers a view engine for the application.
// Children can register their own too. If no Party view Engine is registered
// then this one will be used to render the templates instead.
func (app *Application) RegisterView(viewEngine view.Engine) {
app.view.Register(viewEngine)
}
@ -401,7 +404,7 @@ func (app *Application) RegisterView(viewEngine view.Engine) {
// Use context.View to render templates to the client instead.
// Returns an error on failure, otherwise nil.
func (app *Application) View(writer io.Writer, filename string, layout string, bindingData interface{}) error {
if app.view.Len() == 0 {
if !app.view.Registered() {
err := errors.New("view engine is missing, use `RegisterView`")
app.logger.Error(err)
return err
@ -584,7 +587,7 @@ func (app *Application) Build() error {
}
}
if app.view.Len() == 0 {
if !app.view.Registered() {
for _, s := range []string{"./views", "./templates", "./web/views"} {
if _, err := os.Stat(s); err != nil {
continue
@ -602,13 +605,8 @@ func (app *Application) Build() error {
app.Router.PrependRouterWrapper(app.I18n.Wrapper())
}
if n := app.view.Len(); n > 0 {
tr := "engines"
if n == 1 {
tr = tr[0 : len(tr)-1]
}
app.logger.Debugf("Application: %d registered view %s", n, tr)
if app.view.Registered() {
app.logger.Debugf("Application: view engine %q is registered", app.view.Name())
// view engine
// here is where we declare the closed-relative framework functions.
// Each engine has their defaults, i.e yield,render,render_r,partial, params...

View File

@ -341,6 +341,22 @@ func (app *Application) Party(relativePath string, middleware ...context.Handler
var childNameReplacer = strings.NewReplacer("*", "", "(", "", ")", "")
func getArrowSymbol(static bool, field bool) string {
if field {
if static {
return "╺"
}
return "⦿"
}
if static {
return "•"
}
return "⦿"
}
// TODO: instead of this I want to get in touch with tools like "graphviz"
// so we can put all that information (and the API) inside web graphs,
// it will be easier for developers to see the flow of the whole application,
@ -368,7 +384,11 @@ func logController(logger *golog.Logger, c *ControllerActivator) {
reports := c.injector.Container.Reports
ctrlName := c.RelName()
logger.Debugf("%s\n", ctrlName)
ctrlScopeType := ""
if !c.injector.Singleton {
ctrlScopeType = getArrowSymbol(false, false) + " "
}
logger.Debugf("%s%s\n", ctrlScopeType, ctrlName)
longestNameLen := 0
for _, report := range reports {
@ -462,7 +482,9 @@ func logController(logger *golog.Logger, c *ControllerActivator) {
if spaceRequired < 0 {
spaceRequired = 0
}
fmt.Fprintf(printer, " ╺ %s%s %s\n", fieldName, strings.Repeat(" ", spaceRequired), fileLine)
arrowSymbol := getArrowSymbol(entry.Static, true)
fmt.Fprintf(printer, " %s %s%s %s\n", arrowSymbol, fieldName, strings.Repeat(" ", spaceRequired), fileLine)
}
}
}

View File

@ -19,6 +19,7 @@ import (
// Ace(AssetFile(), ".ace") for embedded data.
func Ace(fs interface{}, extension string) *HTMLEngine {
s := HTML(fs, extension)
s.name = "Ace"
funcs := make(map[string]interface{}, 0)
@ -59,5 +60,6 @@ func Ace(fs interface{}, extension string) *HTMLEngine {
return t.Lookup(name).Tree.Root.String(), nil
}
return s
}

View File

@ -63,7 +63,13 @@ func (s *AmberEngine) RootDir(root string) *AmberEngine {
return s
}
// Name returns the amber engine's name.
func (s *AmberEngine) Name() string {
return "Amber"
}
// Ext returns the file extension which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
func (s *AmberEngine) Ext() string {
return s.extension
}

View File

@ -48,6 +48,11 @@ func Blocks(fs interface{}, extension string) *BlocksEngine {
return WrapBlocks(blocks.New(fs).Extension(extension))
}
// Name returns the blocks engine's name.
func (s *BlocksEngine) Name() string {
return "Blocks"
}
// RootDir sets the directory to use as the root one inside the provided File System.
func (s *BlocksEngine) RootDir(root string) *BlocksEngine {
s.Engine.RootDir(root)

View File

@ -136,7 +136,13 @@ func (s *DjangoEngine) RootDir(root string) *DjangoEngine {
return s
}
// Name returns the django engine's name.
func (s *DjangoEngine) Name() string {
return "Django"
}
// Ext returns the file extension which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
func (s *DjangoEngine) Ext() string {
return s.extension
}

View File

@ -69,7 +69,13 @@ func (s *HandlebarsEngine) RootDir(root string) *HandlebarsEngine {
return s
}
// Name returns the handlebars engine's name.
func (s *HandlebarsEngine) Name() string {
return "Handlebars"
}
// Ext returns the file extension which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
func (s *HandlebarsEngine) Ext() string {
return s.extension
}

View File

@ -14,6 +14,7 @@ import (
// HTMLEngine contains the html view engine structure.
type HTMLEngine struct {
name string // the view engine's name, can be HTML, Ace or Pug.
// the file system to load from.
fs http.FileSystem
// files configuration
@ -81,6 +82,7 @@ var emptyFuncs = template.FuncMap{
// HTML(AssetFile(), ".html") for embedded data.
func HTML(fs interface{}, extension string) *HTMLEngine {
s := &HTMLEngine{
name: "HTML",
fs: getFS(fs),
rootDir: "/",
extension: extension,
@ -102,7 +104,13 @@ func (s *HTMLEngine) RootDir(root string) *HTMLEngine {
return s
}
// Name returns the engine's name.
func (s *HTMLEngine) Name() string {
return s.name
}
// Ext returns the file extension which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
func (s *HTMLEngine) Ext() string {
return s.extension
}
@ -409,7 +417,7 @@ func (s *HTMLEngine) ExecuteWriter(w io.Writer, name string, layout string, bind
if layout = getLayout(layout, s.layout); layout != "" {
lt := s.Templates.Lookup(layout)
if lt == nil {
return ErrNotExist{name, true}
return ErrNotExist{layout, true}
}
s.layoutFuncsFor(lt, name, bindingData)

View File

@ -93,7 +93,13 @@ func (s *JetEngine) RootDir(root string) *JetEngine {
return s
}
// Name returns the jet engine's name.
func (s *JetEngine) Name() string {
return "Jet"
}
// Ext should return the final file extension which this view engine is responsible to render.
// If the filename extension on ExecuteWriter is empty then this is appended.
func (s *JetEngine) Ext() string {
return s.extension
}

View File

@ -25,6 +25,7 @@ import (
// https://github.com/kataras/iris/tree/master/_examples/view/template_pug_3
func Pug(fs interface{}, extension string) *HTMLEngine {
s := HTML(fs, extension)
s.name = "Pug"
s.middleware = func(name string, text []byte) (contents string, err error) {
tmpl := jade.New(name)

View File

@ -3,10 +3,11 @@ package view
import (
"fmt"
"io"
"path/filepath"
"strings"
"github.com/kataras/iris/v12/context"
"github.com/kataras/golog"
)
type (
@ -34,71 +35,65 @@ func (e ErrNotExist) Error() string {
return fmt.Sprintf("%s '%s' does not exist", title, e.Name)
}
// View is responsible to
// load the correct templates
// for each of the registered view engines.
type View struct {
engines []Engine
}
// View is just a wrapper on top of the registered template engine.
type View struct{ Engine }
// Register registers a view engine.
func (v *View) Register(e Engine) {
v.engines = append(v.engines, e)
if v.Engine != nil {
golog.Warnf("Engine already exists, replacing the old %q with the new one %q", v.Engine.Name(), e.Name())
}
v.Engine = e
}
// Find receives a filename, gets its extension and returns the view engine responsible for that file extension
func (v *View) Find(filename string) Engine {
// Read-Only no locks needed, at serve/runtime-time the library is not supposed to add new view engines
for i, n := 0, len(v.engines); i < n; i++ {
e := v.engines[i]
if strings.HasSuffix(filename, e.Ext()) {
return e
// Registered reports whether an engine was registered.
func (v *View) Registered() bool {
return v.Engine != nil
}
func (v *View) ensureTemplateName(s string) string {
if s == "" || s == NoLayout {
return s
}
s = strings.TrimPrefix(s, "/")
if ext := v.Engine.Ext(); ext != "" {
if !strings.HasSuffix(s, ext) {
return s + ext
}
}
return nil
}
// Len returns the length of view engines registered so far.
func (v *View) Len() int {
return len(v.engines)
return s
}
// ExecuteWriter calls the correct view Engine's ExecuteWriter func
func (v *View) ExecuteWriter(w io.Writer, filename string, layout string, bindingData interface{}) error {
if len(filename) > 2 {
if filename[0] == '/' { // omit first slash
filename = filename[1:]
}
}
filename = v.ensureTemplateName(filename)
layout = v.ensureTemplateName(layout)
e := v.Find(filename)
if e == nil {
return fmt.Errorf("no view engine found for '%s'", filepath.Ext(filename))
}
return e.ExecuteWriter(w, filename, layout, bindingData)
return v.Engine.ExecuteWriter(w, filename, layout, bindingData)
}
// AddFunc adds a function to all registered engines.
// Each template engine that supports functions has its own AddFunc too.
func (v *View) AddFunc(funcName string, funcBody interface{}) {
for i, n := 0, len(v.engines); i < n; i++ {
e := v.engines[i]
if engineFuncer, ok := e.(EngineFuncer); ok {
engineFuncer.AddFunc(funcName, funcBody)
}
if !v.Registered() {
return
}
if e, ok := v.Engine.(EngineFuncer); ok {
e.AddFunc(funcName, funcBody)
}
}
// Load compiles all the registered engines.
func (v *View) Load() error {
for i, n := 0, len(v.engines); i < n; i++ {
e := v.engines[i]
if err := e.Load(); err != nil {
return err
}
if !v.Registered() {
return fmt.Errorf("No engine is registered")
}
return nil
return v.Engine.Load()
}
// NoLayout disables the configuration's layout for a specific execution.