1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-17 18:07:01 +00:00

implement #1593 - Read HISTORY.md

updated example: https://github.com/kataras/iris/blob/master/_examples/i18n/main.go#L28-L50
This commit is contained in:
Gerasimos (Makis) Maropoulos
2020-08-18 08:05:51 +03:00
parent 1192e6f787
commit a491cdf7ef
9 changed files with 170 additions and 40 deletions

View File

@@ -359,6 +359,8 @@ Response:
Other Improvements: Other Improvements:
- 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). - 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). - 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).

View File

@@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
) )
@@ -8,12 +10,7 @@ func newApp() *iris.Application {
app := iris.New() app := iris.New()
// Configure i18n. // Configure i18n.
// First parameter: Glob filpath patern, //
// Second variadic parameter: Optional language tags, the first one is the default/fallback one.
err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR", "zh-CN")
if err != nil {
panic(err)
}
// app.I18n.Subdomain = false to disable resolve lang code from subdomain. // app.I18n.Subdomain = false to disable resolve lang code from subdomain.
// app.I18n.LoadAssets for go-bindata. // app.I18n.LoadAssets for go-bindata.
@@ -27,6 +24,31 @@ func newApp() *iris.Application {
// //
// See `app.I18n.ExtractFunc = func(ctx iris.Context) string` or // See `app.I18n.ExtractFunc = func(ctx iris.Context) string` or
// `ctx.SetLanguage(langCode string)` to change the extracted language from a request. // `ctx.SetLanguage(langCode string)` to change the extracted language from a request.
//
// Use DefaultMessageFunc to customize the return value of a not found key or lang.
// All language inputs fallback to the default locale if not matched.
// This is why this one accepts both input and matched languages,
// so the caller can be more expressful knowing those.
// Defaults to nil.
app.I18n.DefaultMessageFunc = func(langInput, langMatched, key string, args ...interface{}) string {
msg := fmt.Sprintf("user language input: %s: matched as: %s: not found key: %s: args: %v", langInput, langMatched, key, args)
app.Logger().Warn(msg)
return msg
}
// Load i18n when customizations are set in place.
//
// First parameter: Glob filpath patern,
// Second variadic parameter: Optional language tags, the first one is the default/fallback one.
err := app.I18n.Load("./locales/*/*.ini", "en-US", "el-GR", "zh-CN")
if err != nil {
panic(err)
}
app.Get("/not-matched", func(ctx iris.Context) {
text := ctx.Tr("not_found_key", "some", "values", 42)
ctx.WriteString(text)
// user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42]
})
app.Get("/", func(ctx iris.Context) { app.Get("/", func(ctx iris.Context) {
hi := ctx.Tr("hi", "iris") hi := ctx.Tr("hi", "iris")

View File

@@ -83,4 +83,6 @@ func TestI18n(t *testing.T) {
e.GET("/el-templates").Expect().Status(httptest.StatusNotFound) e.GET("/el-templates").Expect().Status(httptest.StatusNotFound)
e.GET("/el/templates").Expect().Status(httptest.StatusOK).Body().Contains(elGR).Contains(zhCN) e.GET("/el/templates").Expect().Status(httptest.StatusOK).Body().Contains(elGR).Contains(zhCN)
e.GET("/not-matched").WithQuery("lang", "en-gb").Expect().Status(httptest.StatusOK).Body().Equal("user language input: en-gb: matched as: en-US: not found key: not_found_key: args: [some values 42]")
} }

View File

@@ -752,6 +752,11 @@ type Configuration struct {
// See `i18n.ExtractFunc` for a more organised way of the same feature. // See `i18n.ExtractFunc` for a more organised way of the same feature.
// Defaults to "iris.locale.language". // Defaults to "iris.locale.language".
LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"` LanguageContextKey string `json:"languageContextKey,omitempty" yaml:"LanguageContextKey" toml:"LanguageContextKey"`
// LanguageInputContextKey is the context key of a language that is given by the end-user.
// It's the real user input of the language string, matched or not.
//
// Defaults to "iris.locale.language.input".
LanguageInputContextKey string `json:"languageInputContextKey,omitempty" yaml:"LanguageInputContextKey" toml:"LanguageInputContextKey"`
// VersionContextKey is the context key which an API Version can be modified // VersionContextKey is the context key which an API Version can be modified
// via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`. // via a middleware through `SetVersion` method, e.g. `versioning.SetVersion(ctx, "1.0, 1.1")`.
// Defaults to "iris.api.version". // Defaults to "iris.api.version".
@@ -952,6 +957,11 @@ func (c Configuration) GetLanguageContextKey() string {
return c.LanguageContextKey return c.LanguageContextKey
} }
// GetLanguageInputContextKey returns the LanguageInputContextKey field.
func (c Configuration) GetLanguageInputContextKey() string {
return c.LanguageInputContextKey
}
// GetVersionContextKey returns the VersionContextKey field. // GetVersionContextKey returns the VersionContextKey field.
func (c Configuration) GetVersionContextKey() string { func (c Configuration) GetVersionContextKey() string {
return c.VersionContextKey return c.VersionContextKey
@@ -1107,6 +1117,10 @@ func WithConfiguration(c Configuration) Configurator {
main.LanguageContextKey = v main.LanguageContextKey = v
} }
if v := c.LanguageInputContextKey; v != "" {
main.LanguageInputContextKey = v
}
if v := c.VersionContextKey; v != "" { if v := c.VersionContextKey; v != "" {
main.VersionContextKey = v main.VersionContextKey = v
} }
@@ -1184,15 +1198,16 @@ func DefaultConfiguration() Configuration {
// The request body the size limit // The request body the size limit
// can be set by the middleware `LimitRequestBodySize` // can be set by the middleware `LimitRequestBodySize`
// or `context#SetMaxRequestBodySize`. // or `context#SetMaxRequestBodySize`.
PostMaxMemory: 32 << 20, // 32MB PostMaxMemory: 32 << 20, // 32MB
LocaleContextKey: "iris.locale", LocaleContextKey: "iris.locale",
LanguageContextKey: "iris.locale.language", LanguageContextKey: "iris.locale.language",
VersionContextKey: "iris.api.version", LanguageInputContextKey: "iris.locale.language.input",
ViewEngineContextKey: "iris.view.engine", VersionContextKey: "iris.api.version",
ViewLayoutContextKey: "iris.view.layout", ViewEngineContextKey: "iris.view.engine",
ViewDataContextKey: "iris.view.data", ViewLayoutContextKey: "iris.view.layout",
RemoteAddrHeaders: nil, ViewDataContextKey: "iris.view.data",
RemoteAddrHeadersForce: false, RemoteAddrHeaders: nil,
RemoteAddrHeadersForce: false,
RemoteAddrPrivateSubnets: []netutil.IPRange{ RemoteAddrPrivateSubnets: []netutil.IPRange{
{ {
Start: net.ParseIP("10.0.0.0"), Start: net.ParseIP("10.0.0.0"),

View File

@@ -49,6 +49,8 @@ type ConfigurationReadOnly interface {
GetLocaleContextKey() string GetLocaleContextKey() string
// GetLanguageContextKey returns the LanguageContextKey field. // GetLanguageContextKey returns the LanguageContextKey field.
GetLanguageContextKey() string GetLanguageContextKey() string
// GetLanguageInputContextKey returns the LanguageInputContextKey field.
GetLanguageInputContextKey() string
// GetVersionContextKey returns the VersionContextKey field. // GetVersionContextKey returns the VersionContextKey field.
GetVersionContextKey() string GetVersionContextKey() string

View File

@@ -1203,6 +1203,7 @@ func (ctx *Context) SetLanguage(langCode string) {
} }
// GetLocale returns the current request's `Locale` found by i18n middleware. // GetLocale returns the current request's `Locale` found by i18n middleware.
// It always fallbacks to the default one.
// See `Tr` too. // See `Tr` too.
func (ctx *Context) GetLocale() Locale { func (ctx *Context) GetLocale() Locale {
// Cache the Locale itself for multiple calls of `Tr` method. // Cache the Locale itself for multiple calls of `Tr` method.
@@ -1225,11 +1226,13 @@ func (ctx *Context) GetLocale() Locale {
// See `GetLocale` too. // See `GetLocale` too.
// //
// Example: https://github.com/kataras/iris/tree/master/_examples/i18n // Example: https://github.com/kataras/iris/tree/master/_examples/i18n
func (ctx *Context) Tr(message string, values ...interface{}) string { // other name could be: Localize. func (ctx *Context) Tr(message string, values ...interface{}) string {
if locale := ctx.GetLocale(); locale != nil { // TODO: here... I need to change the logic, if not found then call the i18n's get locale and set the value in order to be fastest on routes that are not using (no need to reigster a middleware.) if locale := ctx.GetLocale(); locale != nil {
return locale.GetMessage(message, values...) return locale.GetMessageContext(ctx, message, values...)
} }
// This should never happen as the locale fallbacks to
// the default.
return message return message
} }

View File

@@ -10,8 +10,8 @@ type I18nReadOnly interface {
Tr(lang string, format string, args ...interface{}) string Tr(lang string, format string, args ...interface{}) string
} }
// Locale is the interface which returns from a `Localizer.GetLocale` metod. // Locale is the interface which returns from a `Localizer.GetLocale` method.
// It serves the transltions based on "key" or format. See `GetMessage`. // It serves the translations based on "key" or format. See `GetMessage`.
type Locale interface { type Locale interface {
// Index returns the current locale index from the languages list. // Index returns the current locale index from the languages list.
Index() int Index() int
@@ -23,6 +23,12 @@ type Locale interface {
// //
// Same as `Tag().String()` but it's static. // Same as `Tag().String()` but it's static.
Language() string Language() string
// GetMessage should return translated text based n the given "key". // GetMessage should return translated text based on the given "key".
GetMessage(key string, args ...interface{}) string GetMessage(key string, args ...interface{}) string
// 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.
GetMessageContext(ctx *Context, key string, args ...interface{}) string
} }

View File

@@ -29,6 +29,19 @@ type (
// It may return the default language if nothing else matches based on custom localizer's criteria. // It may return the default language if nothing else matches based on custom localizer's criteria.
GetLocale(index int) context.Locale 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. // I18n is the structure which keeps the i18n configuration and implements localization and internationalization features.
@@ -43,6 +56,15 @@ type I18n struct {
// to extract the language tag name. // to extract the language tag name.
// Defaults to nil. // Defaults to nil.
ExtractFunc func(ctx *context.Context) string ExtractFunc func(ctx *context.Context) string
// DefaultMessageFunc is the field which can be used
// 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 one accepts both input and matched languages,
// so the caller can be more expressful knowing those.
//
// Defaults to nil.
DefaultMessageFunc MessageFunc
// If not empty, it is language identifier by url query. // If not empty, it is language identifier by url query.
// //
// Defaults to "lang". // Defaults to "lang".
@@ -115,9 +137,10 @@ func (i *I18n) Reset(loader Loader, languages ...string) error {
i.loader = loader i.loader = loader
i.matcher = &Matcher{ i.matcher = &Matcher{
strict: len(tags) > 0, strict: len(tags) > 0,
Languages: tags, Languages: tags,
matcher: language.NewMatcher(tags), matcher: language.NewMatcher(tags),
defaultMessageFunc: i.DefaultMessageFunc,
} }
return i.reload() return i.reload()
@@ -193,6 +216,8 @@ type Matcher struct {
strict bool strict bool
Languages []language.Tag Languages []language.Tag
matcher language.Matcher matcher language.Matcher
// defaultMessageFunc passed by the i18n structure.
defaultMessageFunc MessageFunc
} }
var _ language.Matcher = (*Matcher)(nil) var _ language.Matcher = (*Matcher)(nil)
@@ -295,24 +320,32 @@ func (i *I18n) TryMatchString(s string) (language.Tag, int, bool) {
// Tr returns a translated message based on the "lang" language code // Tr returns a translated message based on the "lang" language code
// and its key(format) with any optional arguments attached to it. // and its key(format) with any optional arguments attached to it.
// //
// It returns an empty string if "format" not matched. // It returns an empty string if "lang" not matched, unless DefaultMessageFunc.
func (i *I18n) Tr(lang, format string, args ...interface{}) string { // It returns the default language's translation if "key" not matched, unless DefaultMessageFunc.
func (i *I18n) Tr(lang, format string, args ...interface{}) (msg string) {
_, index, ok := i.TryMatchString(lang) _, index, ok := i.TryMatchString(lang)
if !ok { if !ok {
index = 0 index = 0
} }
langMatched := ""
loc := i.localizer.GetLocale(index) loc := i.localizer.GetLocale(index)
if loc != nil { if loc != nil {
msg := loc.GetMessage(format, args...) langMatched = loc.Language()
if msg == "" && !i.Strict && index > 0 {
msg = loc.GetMessage(format, args...)
if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && index > 0 {
// it's not the default/fallback language and not message found for that lang:key. // it's not the default/fallback language and not message found for that lang:key.
return i.localizer.GetLocale(0).GetMessage(format, args...) msg = i.localizer.GetLocale(0).GetMessage(format, args...)
} }
return msg
} }
return "" if msg == "" && i.DefaultMessageFunc != nil {
msg = i.DefaultMessageFunc(lang, langMatched, format, args)
}
return
} }
const acceptLanguageHeaderKey = "Accept-Language" const acceptLanguageHeaderKey = "Accept-Language"
@@ -321,12 +354,19 @@ const acceptLanguageHeaderKey = "Accept-Language"
// It will return the first registered language if nothing else matched. // It will return the first registered language if nothing else matched.
func (i *I18n) GetLocale(ctx *context.Context) context.Locale { func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
var ( var (
index int index int
ok bool ok bool
extractedLang string
) )
languageInputKey := ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey()
if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" { if contextKey := ctx.Application().ConfigurationReadOnly().GetLanguageContextKey(); contextKey != "" {
if v := ctx.Values().GetString(contextKey); v != "" { if v := ctx.Values().GetString(contextKey); v != "" {
if languageInputKey != "" {
ctx.Values().Set(languageInputKey, v)
}
if v == "default" { if v == "default" {
index = 0 // no need to call `TryMatchString` and spend time. index = 0 // no need to call `TryMatchString` and spend time.
} else { } else {
@@ -344,30 +384,35 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
if !ok && i.ExtractFunc != nil { if !ok && i.ExtractFunc != nil {
if v := i.ExtractFunc(ctx); v != "" { if v := i.ExtractFunc(ctx); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v) _, index, ok = i.TryMatchString(v)
} }
} }
if !ok && i.URLParameter != "" { if !ok && i.URLParameter != "" {
if v := ctx.URLParam(i.URLParameter); v != "" { if v := ctx.URLParam(i.URLParameter); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v) _, index, ok = i.TryMatchString(v)
} }
} }
if !ok && i.Cookie != "" { if !ok && i.Cookie != "" {
if v := ctx.GetCookie(i.Cookie); v != "" { if v := ctx.GetCookie(i.Cookie); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v) // url.QueryUnescape(cookie.Value) _, index, ok = i.TryMatchString(v) // url.QueryUnescape(cookie.Value)
} }
} }
if !ok && i.Subdomain { if !ok && i.Subdomain {
if v := ctx.Subdomain(); v != "" { if v := ctx.Subdomain(); v != "" {
extractedLang = v
_, index, ok = i.TryMatchString(v) _, index, ok = i.TryMatchString(v)
} }
} }
if !ok { if !ok {
if v := ctx.GetHeader(acceptLanguageHeaderKey); v != "" { if v := ctx.GetHeader(acceptLanguageHeaderKey); v != "" {
extractedLang = v // note.
desired, _, err := language.ParseAcceptLanguage(v) desired, _, err := language.ParseAcceptLanguage(v)
if err == nil { if err == nil {
if _, idx, conf := i.matcher.Match(desired...); conf > language.Low { if _, idx, conf := i.matcher.Match(desired...); conf > language.Low {
@@ -380,8 +425,14 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
// locale := i.localizer.GetLocale(index) // locale := i.localizer.GetLocale(index)
// ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey(), locale) // ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetLocaleContextKey(), locale)
// // if 0 then it defaults to the first language. if languageInputKey != "" {
// return locale // Set the user input we wanna use it on DefaultMessageFunc.
// Even if matched because it may be en-gb or en but if there is a language registered
// as en-us it will be successfully matched ( see TrymatchString and Low conf).
ctx.Values().Set(languageInputKey, extractedLang)
}
// if index == 0 then it defaults to the first language.
locale := i.localizer.GetLocale(index) locale := i.localizer.GetLocale(index)
if locale == nil { if locale == nil {
return nil return nil
@@ -391,18 +442,27 @@ func (i *I18n) GetLocale(ctx *context.Context) context.Locale {
} }
// GetMessage returns the localized text message for this "r" request based on the key "format". // GetMessage returns the localized text message for this "r" request based on the key "format".
// It returns an empty string if locale or format not found. // It returns an empty string if context's locale not matched, unless DefaultMessageFunc.
func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface{}) string { // It returns the default language's translation if "key" not matched, unless DefaultMessageFunc.
func (i *I18n) GetMessage(ctx *context.Context, format string, args ...interface{}) (msg string) {
loc := i.GetLocale(ctx) loc := i.GetLocale(ctx)
langMatched := ""
if loc != nil { if loc != nil {
langMatched = loc.Language()
// it's not the default/fallback language and not message found for that lang:key. // it's not the default/fallback language and not message found for that lang:key.
msg := loc.GetMessage(format, args...) msg = loc.GetMessage(format, args...)
if msg == "" && !i.Strict && loc.Index() > 0 { if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && loc.Index() > 0 {
return i.localizer.GetLocale(0).GetMessage(format, args...) return i.localizer.GetLocale(0).GetMessage(format, args...)
} }
} }
return "" if msg == "" && i.DefaultMessageFunc != nil {
langInput := ctx.Values().GetString(ctx.Application().ConfigurationReadOnly().GetLanguageInputContextKey())
msg = i.DefaultMessageFunc(langInput, langMatched, format, args...)
}
return
} }
func (i *I18n) setLangWithoutContext(w http.ResponseWriter, r *http.Request, lang string) { func (i *I18n) setLangWithoutContext(w http.ResponseWriter, r *http.Request, lang string) {

View File

@@ -149,6 +149,8 @@ func load(assetNames []string, asset func(string) ([]byte, error), options ...Lo
templateKeys: templateKeys, templateKeys: templateKeys,
lineKeys: lineKeys, lineKeys: lineKeys,
other: other, other: other,
defaultMessageFunc: m.defaultMessageFunc,
} }
} }
@@ -217,6 +219,8 @@ type defaultLocale struct {
templateKeys map[string]*template.Template templateKeys map[string]*template.Template
lineKeys map[string]string lineKeys map[string]string
other map[string]interface{} other map[string]interface{}
defaultMessageFunc MessageFunc
} }
func (l *defaultLocale) Index() int { func (l *defaultLocale) Index() int {
@@ -232,6 +236,15 @@ func (l *defaultLocale) Language() string {
} }
func (l *defaultLocale) GetMessage(key string, args ...interface{}) string { 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 {
n := len(args) n := len(args)
if n > 0 { if n > 0 {
// search on templates. // search on templates.
@@ -254,6 +267,11 @@ func (l *defaultLocale) GetMessage(key string, args ...interface{}) string {
return fmt.Sprintf("%v", v) 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 "" return ""
} }